w
This commit is contained in:
+371
-367
@@ -16,9 +16,9 @@ from owa_mcp.server_lifecycle import require_client
|
||||
|
||||
|
||||
def _get_client(ctx: Context) -> OWAClient:
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
app_ctx: AppContext = ctx.request_context.lifespan_context
|
||||
return require_client(app_ctx.client)
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
app_ctx: AppContext = ctx.request_context.lifespan_context
|
||||
return require_client(app_ctx.client)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -27,12 +27,12 @@ def _get_client(ctx: Context) -> OWAClient:
|
||||
|
||||
|
||||
def _resolve_to_email(client: OWAClient, name: str) -> tuple[str, str]:
|
||||
"""Resolve a name/email to (display_name, email). Returns ('','') on failure."""
|
||||
resolutions = client.resolve_names(name)
|
||||
if resolutions:
|
||||
mb = resolutions[0].get("Mailbox", {})
|
||||
return mb.get("Name", name), mb.get("EmailAddress", "")
|
||||
return "", ""
|
||||
"""Resolve a name/email to (display_name, email). Returns ('','') on failure."""
|
||||
resolutions = client.resolve_names(name)
|
||||
if resolutions:
|
||||
mb = resolutions[0].get("Mailbox", {})
|
||||
return mb.get("Name", name), mb.get("EmailAddress", "")
|
||||
return "", ""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -41,105 +41,105 @@ def _resolve_to_email(client: OWAClient, name: str) -> tuple[str, str]:
|
||||
|
||||
|
||||
def _get_availability_events(
|
||||
client: OWAClient,
|
||||
emails: list[str],
|
||||
start: date,
|
||||
end: date,
|
||||
batch_size: int = 5,
|
||||
chunk_days: int = 14,
|
||||
client: OWAClient,
|
||||
emails: list[str],
|
||||
start: date,
|
||||
end: date,
|
||||
batch_size: int = 5,
|
||||
chunk_days: int = 14,
|
||||
) -> dict[str, list[dict]]:
|
||||
"""Query GetUserAvailability for multiple people across a date range.
|
||||
"""Query GetUserAvailability for multiple people across a date range.
|
||||
|
||||
Returns dict mapping email -> list of calendar event dicts with
|
||||
keys: subject, start_date, busy_type.
|
||||
"""
|
||||
results: dict[str, list[dict]] = {email: [] for email in emails}
|
||||
Returns dict mapping email -> list of calendar event dicts with
|
||||
keys: subject, start_date, busy_type.
|
||||
"""
|
||||
results: dict[str, list[dict]] = {email: [] for email in emails}
|
||||
|
||||
# Batch people
|
||||
email_batches = [
|
||||
emails[i : i + batch_size] for i in range(0, len(emails), batch_size)
|
||||
]
|
||||
# Batch people
|
||||
email_batches = [
|
||||
emails[i : i + batch_size] for i in range(0, len(emails), batch_size)
|
||||
]
|
||||
|
||||
for batch in email_batches:
|
||||
current = start
|
||||
while current < end:
|
||||
chunk_end = min(current + timedelta(days=chunk_days), end)
|
||||
for batch in email_batches:
|
||||
current = start
|
||||
while current < end:
|
||||
chunk_end = min(current + timedelta(days=chunk_days), end)
|
||||
|
||||
mailbox_data = [
|
||||
mailbox_data = [
|
||||
{
|
||||
"__type": "MailboxData:#Exchange",
|
||||
"Email": {
|
||||
"__type": "EmailAddress:#Exchange",
|
||||
"Address": email,
|
||||
},
|
||||
"AttendeeType": "Required",
|
||||
}
|
||||
for email in batch
|
||||
]
|
||||
|
||||
payload = {
|
||||
"__type": "GetUserAvailabilityJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
"TimeZoneContext": {
|
||||
"__type": "TimeZoneContext:#Exchange",
|
||||
"TimeZoneDefinition": {
|
||||
"__type": "TimeZoneDefinitionType:#Exchange",
|
||||
"Id": "Russian Standard Time",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetUserAvailabilityRequest:#Exchange",
|
||||
"MailboxDataArray": mailbox_data,
|
||||
"FreeBusyViewOptions": {
|
||||
"__type": "FreeBusyViewOptions:#Exchange",
|
||||
"TimeWindow": {
|
||||
"__type": "Duration:#Exchange",
|
||||
"StartTime": f"{current}T00:00:00",
|
||||
"EndTime": f"{chunk_end}T00:00:00",
|
||||
},
|
||||
"MergedFreeBusyIntervalInMinutes": 30,
|
||||
"RequestedView": "DetailedMerged",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request("GetUserAvailability", payload)
|
||||
body = data.get("Body", {})
|
||||
for i, fb_resp in enumerate(body.get("FreeBusyResponseArray", [])):
|
||||
if i >= len(batch):
|
||||
break
|
||||
email = batch[i]
|
||||
fb_view = fb_resp.get("FreeBusyView", {})
|
||||
cal_events = fb_view.get("CalendarEventArray", {})
|
||||
if isinstance(cal_events, dict):
|
||||
items = cal_events.get("Items", [])
|
||||
elif isinstance(cal_events, list):
|
||||
items = cal_events
|
||||
else:
|
||||
items = []
|
||||
for event in items:
|
||||
s = event.get("StartTime", "")
|
||||
bt = event.get("BusyType", "")
|
||||
if s and bt != "Free":
|
||||
details = event.get("CalendarEventDetails", {})
|
||||
subject = details.get("Subject", "") if details else ""
|
||||
results[email].append(
|
||||
{
|
||||
"__type": "MailboxData:#Exchange",
|
||||
"Email": {
|
||||
"__type": "EmailAddress:#Exchange",
|
||||
"Address": email,
|
||||
},
|
||||
"AttendeeType": "Required",
|
||||
"subject": subject,
|
||||
"start_date": s[:10],
|
||||
"busy_type": bt,
|
||||
}
|
||||
for email in batch
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
payload = {
|
||||
"__type": "GetUserAvailabilityJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
"TimeZoneContext": {
|
||||
"__type": "TimeZoneContext:#Exchange",
|
||||
"TimeZoneDefinition": {
|
||||
"__type": "TimeZoneDefinitionType:#Exchange",
|
||||
"Id": "Russian Standard Time",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetUserAvailabilityRequest:#Exchange",
|
||||
"MailboxDataArray": mailbox_data,
|
||||
"FreeBusyViewOptions": {
|
||||
"__type": "FreeBusyViewOptions:#Exchange",
|
||||
"TimeWindow": {
|
||||
"__type": "Duration:#Exchange",
|
||||
"StartTime": f"{current}T00:00:00",
|
||||
"EndTime": f"{chunk_end}T00:00:00",
|
||||
},
|
||||
"MergedFreeBusyIntervalInMinutes": 30,
|
||||
"RequestedView": "DetailedMerged",
|
||||
},
|
||||
},
|
||||
}
|
||||
current = chunk_end
|
||||
|
||||
try:
|
||||
data = client.request("GetUserAvailability", payload)
|
||||
body = data.get("Body", {})
|
||||
for i, fb_resp in enumerate(body.get("FreeBusyResponseArray", [])):
|
||||
if i >= len(batch):
|
||||
break
|
||||
email = batch[i]
|
||||
fb_view = fb_resp.get("FreeBusyView", {})
|
||||
cal_events = fb_view.get("CalendarEventArray", {})
|
||||
if isinstance(cal_events, dict):
|
||||
items = cal_events.get("Items", [])
|
||||
elif isinstance(cal_events, list):
|
||||
items = cal_events
|
||||
else:
|
||||
items = []
|
||||
for event in items:
|
||||
s = event.get("StartTime", "")
|
||||
bt = event.get("BusyType", "")
|
||||
if s and bt != "Free":
|
||||
details = event.get("CalendarEventDetails", {})
|
||||
subject = details.get("Subject", "") if details else ""
|
||||
results[email].append(
|
||||
{
|
||||
"subject": subject,
|
||||
"start_date": s[:10],
|
||||
"busy_type": bt,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
current = chunk_end
|
||||
|
||||
return results
|
||||
return results
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -149,105 +149,107 @@ def _get_availability_events(
|
||||
|
||||
@mcp.tool()
|
||||
def get_meeting_stats(
|
||||
people: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
ctx: Context = None,
|
||||
people: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Get meeting count statistics for one or more people over a date range.
|
||||
"""Get meeting count statistics for one or more people over a date range.
|
||||
|
||||
Uses GetUserAvailability to count calendar events (including
|
||||
expanded recurring instances) for each person.
|
||||
Uses GetUserAvailability to count calendar events (including
|
||||
expanded recurring instances) for each person.
|
||||
|
||||
Args:
|
||||
people: Comma-separated names or email addresses to analyze.
|
||||
start_date: Start date in YYYY-MM-DD format.
|
||||
end_date: End date in YYYY-MM-DD format.
|
||||
Args:
|
||||
people: Comma-separated names or email addresses to analyze.
|
||||
start_date: Start date in YYYY-MM-DD format.
|
||||
end_date: End date in YYYY-MM-DD format.
|
||||
|
||||
Returns:
|
||||
JSON object with per-person stats sorted by meeting count:
|
||||
total_meetings, meetings_per_workday, days_with_meetings.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with per-person stats sorted by meeting count:
|
||||
total_meetings, meetings_per_workday, days_with_meetings.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": f"Invalid date format: {e}"})
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": f"Invalid date format: {e}"})
|
||||
|
||||
# Resolve names
|
||||
name_list = [n.strip() for n in people.split(",") if n.strip()]
|
||||
if not name_list:
|
||||
return json.dumps({"error": "No people specified."})
|
||||
# Resolve names
|
||||
name_list = [n.strip() for n in people.split(",") if n.strip()]
|
||||
if not name_list:
|
||||
return json.dumps({"error": "No people specified."})
|
||||
|
||||
resolved = [] # (display_name, email)
|
||||
for name in name_list:
|
||||
display, email = _resolve_to_email(client, name)
|
||||
if email:
|
||||
resolved.append((display, email))
|
||||
else:
|
||||
resolved.append((name, ""))
|
||||
|
||||
emails = [email for _, email in resolved if email]
|
||||
if not emails:
|
||||
return json.dumps({"error": "Could not resolve any names to email addresses."})
|
||||
|
||||
# Query availability
|
||||
avail = _get_availability_events(client, emails, sd, ed)
|
||||
|
||||
# Count working days
|
||||
workdays = 0
|
||||
d = sd
|
||||
while d < ed:
|
||||
if d.weekday() < 5:
|
||||
workdays += 1
|
||||
d += timedelta(days=1)
|
||||
workdays = max(workdays, 1)
|
||||
|
||||
# Build stats
|
||||
stats = []
|
||||
for display, email in resolved:
|
||||
if not email or email not in avail:
|
||||
stats.append(
|
||||
{
|
||||
"name": display,
|
||||
"email": email or "not_found",
|
||||
"total_meetings": 0,
|
||||
"meetings_per_workday": 0,
|
||||
"days_with_meetings": 0,
|
||||
"workdays": workdays,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
events = avail[email]
|
||||
total = len(events)
|
||||
unique_days = len(set(ev["start_date"] for ev in events))
|
||||
avg = round(total / workdays, 1)
|
||||
|
||||
stats.append(
|
||||
{
|
||||
"name": display,
|
||||
"email": email,
|
||||
"total_meetings": total,
|
||||
"meetings_per_workday": avg,
|
||||
"days_with_meetings": unique_days,
|
||||
"workdays": workdays,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort by total descending
|
||||
stats.sort(key=lambda x: x["total_meetings"], reverse=True)
|
||||
resolved = [] # (display_name, email)
|
||||
for name in name_list:
|
||||
display, email = _resolve_to_email(client, name)
|
||||
if email:
|
||||
resolved.append((display, email))
|
||||
else:
|
||||
resolved.append((name, ""))
|
||||
|
||||
emails = [email for _, email in resolved if email]
|
||||
if not emails:
|
||||
return json.dumps(
|
||||
{
|
||||
"period": {"start": start_date, "end": end_date, "workdays": workdays},
|
||||
"stats": stats,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
{"error": "Could not resolve any names to email addresses."}
|
||||
)
|
||||
|
||||
# Query availability
|
||||
avail = _get_availability_events(client, emails, sd, ed)
|
||||
|
||||
# Count working days
|
||||
workdays = 0
|
||||
d = sd
|
||||
while d < ed:
|
||||
if d.weekday() < 5:
|
||||
workdays += 1
|
||||
d += timedelta(days=1)
|
||||
workdays = max(workdays, 1)
|
||||
|
||||
# Build stats
|
||||
stats = []
|
||||
for display, email in resolved:
|
||||
if not email or email not in avail:
|
||||
stats.append(
|
||||
{
|
||||
"name": display,
|
||||
"email": email or "not_found",
|
||||
"total_meetings": 0,
|
||||
"meetings_per_workday": 0,
|
||||
"days_with_meetings": 0,
|
||||
"workdays": workdays,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
events = avail[email]
|
||||
total = len(events)
|
||||
unique_days = len(set(ev["start_date"] for ev in events))
|
||||
avg = round(total / workdays, 1)
|
||||
|
||||
stats.append(
|
||||
{
|
||||
"name": display,
|
||||
"email": email,
|
||||
"total_meetings": total,
|
||||
"meetings_per_workday": avg,
|
||||
"days_with_meetings": unique_days,
|
||||
"workdays": workdays,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort by total descending
|
||||
stats.sort(key=lambda x: x["total_meetings"], reverse=True)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"period": {"start": start_date, "end": end_date, "workdays": workdays},
|
||||
"stats": stats,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool: get_meeting_contacts
|
||||
@@ -256,194 +258,196 @@ def get_meeting_stats(
|
||||
|
||||
@mcp.tool()
|
||||
def get_meeting_contacts(
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
top_n: int = 30,
|
||||
ctx: Context = None,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
top_n: int = 30,
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Build a connection matrix: who you meet with most often.
|
||||
"""Build a connection matrix: who you meet with most often.
|
||||
|
||||
Analyzes your calendar over a date range, accounting for recurring
|
||||
meetings. For each contact found in your meetings, returns the
|
||||
weighted count of shared meetings.
|
||||
Analyzes your calendar over a date range, accounting for recurring
|
||||
meetings. For each contact found in your meetings, returns the
|
||||
weighted count of shared meetings.
|
||||
|
||||
Args:
|
||||
start_date: Start date in YYYY-MM-DD format.
|
||||
end_date: End date in YYYY-MM-DD format.
|
||||
top_n: Number of top contacts to return. Default 30.
|
||||
Args:
|
||||
start_date: Start date in YYYY-MM-DD format.
|
||||
end_date: End date in YYYY-MM-DD format.
|
||||
top_n: Number of top contacts to return. Default 30.
|
||||
|
||||
Returns:
|
||||
JSON object with total_meetings, unique_contacts, and a ranked
|
||||
contacts array of {name, email, meetings} objects.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with total_meetings, unique_contacts, and a ranked
|
||||
contacts array of {name, email, meetings} objects.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": f"Invalid date format: {e}"})
|
||||
|
||||
# Step 1: Get own expanded events via GetUserAvailability
|
||||
# to count subject occurrences (handles recurring meetings)
|
||||
own_email = client.user_email.lower() if client.user_email else ""
|
||||
if not own_email:
|
||||
return json.dumps(
|
||||
{"error": "User email not available. Call the login tool first."}
|
||||
)
|
||||
|
||||
folder_id = client.get_folder_id("calendar")
|
||||
if not folder_id:
|
||||
return json.dumps({"error": "Could not find calendar folder."})
|
||||
|
||||
# Get expanded event subjects with occurrence counts
|
||||
avail_result = _get_availability_events(client, [client.user_email], sd, ed)
|
||||
expanded_events = avail_result.get(client.user_email, [])
|
||||
|
||||
subject_counts = Counter(ev["subject"] for ev in expanded_events)
|
||||
total_expanded = len(expanded_events)
|
||||
|
||||
# Step 2: Find master calendar items for each unique subject
|
||||
# Scan entire calendar (descending) to match subjects
|
||||
subject_to_id: dict[str, str] = {}
|
||||
offset = 0
|
||||
|
||||
while len(subject_to_id) < len(subject_counts):
|
||||
payload = {
|
||||
"__type": "FindItemJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "FindItemRequest:#Exchange",
|
||||
"ItemShape": {
|
||||
"__type": "ItemResponseShape:#Exchange",
|
||||
"BaseShape": "Default",
|
||||
},
|
||||
"ParentFolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"Traversal": "Shallow",
|
||||
"Paging": {
|
||||
"__type": "IndexedPageView:#Exchange",
|
||||
"BasePoint": "Beginning",
|
||||
"Offset": offset,
|
||||
"MaxEntriesReturned": 200,
|
||||
},
|
||||
"SortOrder": [
|
||||
{
|
||||
"__type": "SortResults:#Exchange",
|
||||
"Order": "Descending",
|
||||
"Path": {
|
||||
"__type": "PropertyUri:#Exchange",
|
||||
"FieldURI": "Start",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
data = client.request("FindItem", payload)
|
||||
items = client.extract_items(data)
|
||||
if not items:
|
||||
break
|
||||
folder_items = items[0].get("RootFolder", {}).get("Items", [])
|
||||
is_last = (
|
||||
items[0].get("RootFolder", {}).get("IncludesLastItemInRange", True)
|
||||
)
|
||||
if not folder_items:
|
||||
break
|
||||
|
||||
for item in folder_items:
|
||||
subj = item.get("Subject", "")
|
||||
if subj and subj in subject_counts and subj not in subject_to_id:
|
||||
subject_to_id[subj] = item.get("ItemId", {}).get("Id", "")
|
||||
|
||||
if is_last:
|
||||
break
|
||||
offset += 200
|
||||
if offset > 3000:
|
||||
break
|
||||
|
||||
# Step 3: GetItem for each master to get attendees
|
||||
unique_ids = list(set(subject_to_id.values()))
|
||||
id_to_attendees: dict[str, set] = {}
|
||||
|
||||
for bi in range(0, len(unique_ids), 10):
|
||||
batch = unique_ids[bi : bi + 10]
|
||||
item_id_list = [{"__type": "ItemId:#Exchange", "Id": iid} for iid in batch]
|
||||
|
||||
payload = {
|
||||
"__type": "GetItemJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetItemRequest:#Exchange",
|
||||
"ItemShape": {
|
||||
"__type": "ItemResponseShape:#Exchange",
|
||||
"BaseShape": "AllProperties",
|
||||
},
|
||||
"ItemIds": item_id_list,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": f"Invalid date format: {e}"})
|
||||
data = client.request("GetItem", payload)
|
||||
for msg in client.extract_items(data):
|
||||
if "Items" not in msg:
|
||||
continue
|
||||
for item in msg["Items"]:
|
||||
item_id = item.get("ItemId", {}).get("Id", "")
|
||||
attendees = set()
|
||||
|
||||
# Step 1: Get own expanded events via GetUserAvailability
|
||||
# to count subject occurrences (handles recurring meetings)
|
||||
own_email = client.user_email.lower() if client.user_email else ""
|
||||
if not own_email:
|
||||
return json.dumps(
|
||||
{"error": "User email not available. Call the login tool first."}
|
||||
)
|
||||
|
||||
folder_id = client.get_folder_id("calendar")
|
||||
if not folder_id:
|
||||
return json.dumps({"error": "Could not find calendar folder."})
|
||||
|
||||
# Get expanded event subjects with occurrence counts
|
||||
avail_result = _get_availability_events(client, [client.user_email], sd, ed)
|
||||
expanded_events = avail_result.get(client.user_email, [])
|
||||
|
||||
subject_counts = Counter(ev["subject"] for ev in expanded_events)
|
||||
total_expanded = len(expanded_events)
|
||||
|
||||
# Step 2: Find master calendar items for each unique subject
|
||||
# Scan entire calendar (descending) to match subjects
|
||||
subject_to_id: dict[str, str] = {}
|
||||
offset = 0
|
||||
|
||||
while len(subject_to_id) < len(subject_counts):
|
||||
payload = {
|
||||
"__type": "FindItemJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "FindItemRequest:#Exchange",
|
||||
"ItemShape": {
|
||||
"__type": "ItemResponseShape:#Exchange",
|
||||
"BaseShape": "Default",
|
||||
},
|
||||
"ParentFolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"Traversal": "Shallow",
|
||||
"Paging": {
|
||||
"__type": "IndexedPageView:#Exchange",
|
||||
"BasePoint": "Beginning",
|
||||
"Offset": offset,
|
||||
"MaxEntriesReturned": 200,
|
||||
},
|
||||
"SortOrder": [
|
||||
{
|
||||
"__type": "SortResults:#Exchange",
|
||||
"Order": "Descending",
|
||||
"Path": {
|
||||
"__type": "PropertyUri:#Exchange",
|
||||
"FieldURI": "Start",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
data = client.request("FindItem", payload)
|
||||
items = client.extract_items(data)
|
||||
if not items:
|
||||
break
|
||||
folder_items = items[0].get("RootFolder", {}).get("Items", [])
|
||||
is_last = items[0].get("RootFolder", {}).get("IncludesLastItemInRange", True)
|
||||
if not folder_items:
|
||||
break
|
||||
|
||||
for item in folder_items:
|
||||
subj = item.get("Subject", "")
|
||||
if subj and subj in subject_counts and subj not in subject_to_id:
|
||||
subject_to_id[subj] = item.get("ItemId", {}).get("Id", "")
|
||||
|
||||
if is_last:
|
||||
break
|
||||
offset += 200
|
||||
if offset > 3000:
|
||||
break
|
||||
|
||||
# Step 3: GetItem for each master to get attendees
|
||||
unique_ids = list(set(subject_to_id.values()))
|
||||
id_to_attendees: dict[str, set] = {}
|
||||
|
||||
for bi in range(0, len(unique_ids), 10):
|
||||
batch = unique_ids[bi : bi + 10]
|
||||
item_id_list = [{"__type": "ItemId:#Exchange", "Id": iid} for iid in batch]
|
||||
|
||||
payload = {
|
||||
"__type": "GetItemJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetItemRequest:#Exchange",
|
||||
"ItemShape": {
|
||||
"__type": "ItemResponseShape:#Exchange",
|
||||
"BaseShape": "AllProperties",
|
||||
},
|
||||
"ItemIds": item_id_list,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request("GetItem", payload)
|
||||
for msg in client.extract_items(data):
|
||||
if "Items" not in msg:
|
||||
continue
|
||||
for item in msg["Items"]:
|
||||
item_id = item.get("ItemId", {}).get("Id", "")
|
||||
attendees = set()
|
||||
|
||||
for att_list in [
|
||||
item.get("RequiredAttendees", []) or [],
|
||||
item.get("OptionalAttendees", []) or [],
|
||||
]:
|
||||
for a in att_list:
|
||||
mailbox = a.get("Mailbox", {})
|
||||
name = mailbox.get("Name", "")
|
||||
addr = mailbox.get("EmailAddress", "")
|
||||
if not name or not addr or addr.startswith("/O="):
|
||||
continue
|
||||
attendees.add((name, addr.lower()))
|
||||
|
||||
organizer = item.get("Organizer", {}).get("Mailbox", {})
|
||||
if organizer:
|
||||
org_addr = organizer.get("EmailAddress", "")
|
||||
org_name = organizer.get("Name", "")
|
||||
if org_addr and not org_addr.startswith("/O="):
|
||||
attendees.add((org_name, org_addr.lower()))
|
||||
|
||||
id_to_attendees[item_id] = attendees
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Step 4: Build weighted contacts, excluding self
|
||||
contacts = Counter()
|
||||
for subject, item_id in subject_to_id.items():
|
||||
weight = subject_counts.get(subject, 1)
|
||||
attendees = id_to_attendees.get(item_id, set())
|
||||
for name, email in attendees:
|
||||
if own_email and email == own_email:
|
||||
for att_list in [
|
||||
item.get("RequiredAttendees", []) or [],
|
||||
item.get("OptionalAttendees", []) or [],
|
||||
]:
|
||||
for a in att_list:
|
||||
mailbox = a.get("Mailbox", {})
|
||||
name = mailbox.get("Name", "")
|
||||
addr = mailbox.get("EmailAddress", "")
|
||||
if not name or not addr or addr.startswith("/O="):
|
||||
continue
|
||||
contacts[(name, email)] += weight
|
||||
attendees.add((name, addr.lower()))
|
||||
|
||||
result_contacts = []
|
||||
for (name, email), count in contacts.most_common(top_n):
|
||||
result_contacts.append(
|
||||
{
|
||||
"name": name,
|
||||
"email": email,
|
||||
"meetings": count,
|
||||
}
|
||||
)
|
||||
organizer = item.get("Organizer", {}).get("Mailbox", {})
|
||||
if organizer:
|
||||
org_addr = organizer.get("EmailAddress", "")
|
||||
org_name = organizer.get("Name", "")
|
||||
if org_addr and not org_addr.startswith("/O="):
|
||||
attendees.add((org_name, org_addr.lower()))
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"period": {"start": start_date, "end": end_date},
|
||||
"total_meetings": total_expanded,
|
||||
"unique_contacts": len(contacts),
|
||||
"contacts": result_contacts,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
id_to_attendees[item_id] = attendees
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Step 4: Build weighted contacts, excluding self
|
||||
contacts = Counter()
|
||||
for subject, item_id in subject_to_id.items():
|
||||
weight = subject_counts.get(subject, 1)
|
||||
attendees = id_to_attendees.get(item_id, set())
|
||||
for name, email in attendees:
|
||||
if own_email and email == own_email:
|
||||
continue
|
||||
contacts[(name, email)] += weight
|
||||
|
||||
result_contacts = []
|
||||
for (name, email), count in contacts.most_common(top_n):
|
||||
result_contacts.append(
|
||||
{
|
||||
"name": name,
|
||||
"email": email,
|
||||
"meetings": count,
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"period": {"start": start_date, "end": end_date},
|
||||
"total_meetings": total_expanded,
|
||||
"unique_contacts": len(contacts),
|
||||
"contacts": result_contacts,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
+85
-85
@@ -18,108 +18,108 @@ from owa_mcp.server_lifecycle import require_client
|
||||
|
||||
|
||||
def _get_app_ctx(ctx: Context) -> AppContext:
|
||||
"""Extract the AppContext from the MCP lifespan context."""
|
||||
return ctx.request_context.lifespan_context
|
||||
"""Extract the AppContext from the MCP lifespan context."""
|
||||
return ctx.request_context.lifespan_context
|
||||
|
||||
|
||||
def _get_client(ctx: Context) -> OWAClient:
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
return require_client(_get_app_ctx(ctx).client)
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
return require_client(_get_app_ctx(ctx).client)
|
||||
|
||||
|
||||
def _session_is_valid(client: OWAClient) -> bool:
|
||||
"""Quick check: can we reach the inbox?"""
|
||||
try:
|
||||
client._ensure_loaded()
|
||||
payload = {
|
||||
"__type": "GetFolderJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetFolderRequest:#Exchange",
|
||||
"FolderShape": {
|
||||
"__type": "FolderResponseShape:#Exchange",
|
||||
"BaseShape": "IdOnly",
|
||||
},
|
||||
"FolderIds": [
|
||||
{
|
||||
"__type": "DistinguishedFolderId:#Exchange",
|
||||
"Id": "inbox",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
data = client.request("GetFolder", payload)
|
||||
items = client.extract_items(data)
|
||||
return bool(items)
|
||||
except Exception:
|
||||
return False
|
||||
"""Quick check: can we reach the inbox?"""
|
||||
try:
|
||||
client._ensure_loaded()
|
||||
payload = {
|
||||
"__type": "GetFolderJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetFolderRequest:#Exchange",
|
||||
"FolderShape": {
|
||||
"__type": "FolderResponseShape:#Exchange",
|
||||
"BaseShape": "IdOnly",
|
||||
},
|
||||
"FolderIds": [
|
||||
{
|
||||
"__type": "DistinguishedFolderId:#Exchange",
|
||||
"Id": "inbox",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
data = client.request("GetFolder", payload)
|
||||
items = client.extract_items(data)
|
||||
return bool(items)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def login(
|
||||
cookies: str,
|
||||
ctx: Context = None,
|
||||
cookies: str,
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Authenticate to Exchange OWA using session cookies from the browser.
|
||||
"""Authenticate to Exchange OWA using session cookies from the browser.
|
||||
|
||||
Paste the cookies you copied from your browser (DevTools → Application →
|
||||
Cookies) as a ``name=value`` string — one cookie per line.
|
||||
Paste the cookies you copied from your browser (DevTools → Application →
|
||||
Cookies) as a ``name=value`` string — one cookie per line.
|
||||
|
||||
Example cookies string::
|
||||
Example cookies string::
|
||||
|
||||
X-OWA-CANARY=abc123
|
||||
CookieAuth1=def456
|
||||
...
|
||||
X-OWA-CANARY=abc123
|
||||
CookieAuth1=def456
|
||||
...
|
||||
|
||||
Args:
|
||||
cookies: Session cookies in ``name=value`` format, one per line.
|
||||
Obtain from browser DevTools → Application → Cookies → copy all
|
||||
cookies as ``name=value`` pairs.
|
||||
Args:
|
||||
cookies: Session cookies in ``name=value`` format, one per line.
|
||||
Obtain from browser DevTools → Application → Cookies → copy all
|
||||
cookies as ``name=value`` pairs.
|
||||
|
||||
Returns:
|
||||
JSON result with success status and message.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON result with success status and message.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
# Parse and validate cookies
|
||||
cookies_str = cookies.strip()
|
||||
if not cookies_str:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Cookies string is empty. Paste your browser cookies as name=value pairs, one per line.",
|
||||
}
|
||||
)
|
||||
# Parse and validate cookies
|
||||
cookies_str = cookies.strip()
|
||||
if not cookies_str:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Cookies string is empty. Paste your browser cookies as name=value pairs, one per line.",
|
||||
}
|
||||
)
|
||||
|
||||
# Save to disk
|
||||
try:
|
||||
client.cookie_file.write_text(cookies_str + "\n")
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"success": False, "error": f"Failed to write cookie file: {e}"}
|
||||
)
|
||||
# Save to disk
|
||||
try:
|
||||
client.cookie_file.write_text(cookies_str + "\n")
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"success": False, "error": f"Failed to write cookie file: {e}"}
|
||||
)
|
||||
|
||||
# Load into memory
|
||||
try:
|
||||
client.load_cookies_from_string(cookies_str)
|
||||
except SessionExpiredError as e:
|
||||
return json.dumps({"success": False, "error": str(e)})
|
||||
# Load into memory
|
||||
try:
|
||||
client.load_cookies_from_string(cookies_str)
|
||||
except SessionExpiredError as e:
|
||||
return json.dumps({"success": False, "error": str(e)})
|
||||
|
||||
# Verify session
|
||||
if _session_is_valid(client):
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Logged in. Session cookies saved to {client.cookie_file}.",
|
||||
}
|
||||
)
|
||||
else:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Cookies loaded but session verification failed. The cookies may have expired.",
|
||||
}
|
||||
)
|
||||
# Verify session
|
||||
if _session_is_valid(client):
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Logged in. Session cookies saved to {client.cookie_file}.",
|
||||
}
|
||||
)
|
||||
else:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Cookies loaded but session verification failed. The cookies may have expired.",
|
||||
}
|
||||
)
|
||||
|
||||
+499
-495
File diff suppressed because it is too large
Load Diff
+1148
-1157
File diff suppressed because it is too large
Load Diff
+998
-997
File diff suppressed because it is too large
Load Diff
+343
-341
@@ -12,435 +12,437 @@ from owa_mcp.server import AppContext, mcp
|
||||
from owa_mcp.server_lifecycle import require_client
|
||||
|
||||
_DISTINGUISHED_NAMES = {
|
||||
"msgfolderroot",
|
||||
"inbox",
|
||||
"sentitems",
|
||||
"drafts",
|
||||
"deleteditems",
|
||||
"junkemail",
|
||||
"outbox",
|
||||
"calendar",
|
||||
"contacts",
|
||||
"tasks",
|
||||
"notes",
|
||||
"journal",
|
||||
"searchfolders",
|
||||
"msgfolderroot",
|
||||
"inbox",
|
||||
"sentitems",
|
||||
"drafts",
|
||||
"deleteditems",
|
||||
"junkemail",
|
||||
"outbox",
|
||||
"calendar",
|
||||
"contacts",
|
||||
"tasks",
|
||||
"notes",
|
||||
"journal",
|
||||
"searchfolders",
|
||||
}
|
||||
|
||||
_HEADER_TZ = {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
"TimeZoneContext": {
|
||||
"__type": "TimeZoneContext:#Exchange",
|
||||
"TimeZoneDefinition": {
|
||||
"__type": "TimeZoneDefinitionType:#Exchange",
|
||||
"Id": "Russian Standard Time",
|
||||
},
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
"TimeZoneContext": {
|
||||
"__type": "TimeZoneContext:#Exchange",
|
||||
"TimeZoneDefinition": {
|
||||
"__type": "TimeZoneDefinitionType:#Exchange",
|
||||
"Id": "Russian Standard Time",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _get_client(ctx: Context) -> OWAClient:
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
app_ctx: AppContext = ctx.request_context.lifespan_context
|
||||
return require_client(app_ctx.client)
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
app_ctx: AppContext = ctx.request_context.lifespan_context
|
||||
return require_client(app_ctx.client)
|
||||
|
||||
|
||||
def _folder_id_dict(folder_id: str) -> dict:
|
||||
"""Build a typed FolderId or DistinguishedFolderId dict."""
|
||||
if folder_id.lower() in _DISTINGUISHED_NAMES:
|
||||
return {"__type": "DistinguishedFolderId:#Exchange", "Id": folder_id}
|
||||
return {"__type": "FolderId:#Exchange", "Id": folder_id}
|
||||
"""Build a typed FolderId or DistinguishedFolderId dict."""
|
||||
if folder_id.lower() in _DISTINGUISHED_NAMES:
|
||||
return {"__type": "DistinguishedFolderId:#Exchange", "Id": folder_id}
|
||||
return {"__type": "FolderId:#Exchange", "Id": folder_id}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def check_session(ctx: Context = None) -> str:
|
||||
"""Check whether the current OWA session is authenticated.
|
||||
"""Check whether the current OWA session is authenticated.
|
||||
|
||||
Makes a lightweight GetFolder call on the inbox. Returns session
|
||||
status, the mailbox display name, and cookie file path.
|
||||
Makes a lightweight GetFolder call on the inbox. Returns session
|
||||
status, the mailbox display name, and cookie file path.
|
||||
|
||||
Returns:
|
||||
JSON object with authenticated (bool), mailbox name, and details.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with authenticated (bool), mailbox name, and details.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
payload = {
|
||||
"__type": "GetFolderJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetFolderRequest:#Exchange",
|
||||
"FolderShape": {
|
||||
"__type": "FolderResponseShape:#Exchange",
|
||||
"BaseShape": "Default",
|
||||
},
|
||||
"FolderIds": [{"__type": "DistinguishedFolderId:#Exchange", "Id": "inbox"}],
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request("GetFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"authenticated": False,
|
||||
"error": str(e),
|
||||
"cookie_file": str(client.cookie_file),
|
||||
}
|
||||
)
|
||||
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"authenticated": True,
|
||||
"mailbox": folder.get("DisplayName", ""),
|
||||
"unread": folder.get("UnreadCount", 0),
|
||||
"cookie_file": str(client.cookie_file),
|
||||
}
|
||||
)
|
||||
payload = {
|
||||
"__type": "GetFolderJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "GetFolderRequest:#Exchange",
|
||||
"FolderShape": {
|
||||
"__type": "FolderResponseShape:#Exchange",
|
||||
"BaseShape": "Default",
|
||||
},
|
||||
"FolderIds": [
|
||||
{"__type": "DistinguishedFolderId:#Exchange", "Id": "inbox"}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request("GetFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"authenticated": False,
|
||||
"error": "Unexpected response",
|
||||
"cookie_file": str(client.cookie_file),
|
||||
}
|
||||
{
|
||||
"authenticated": False,
|
||||
"error": str(e),
|
||||
"cookie_file": str(client.cookie_file),
|
||||
}
|
||||
)
|
||||
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"authenticated": True,
|
||||
"mailbox": folder.get("DisplayName", ""),
|
||||
"unread": folder.get("UnreadCount", 0),
|
||||
"cookie_file": str(client.cookie_file),
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"authenticated": False,
|
||||
"error": "Unexpected response",
|
||||
"cookie_file": str(client.cookie_file),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_folders(
|
||||
parent_folder_id: str = "msgfolderroot",
|
||||
recursive: bool = False,
|
||||
ctx: Context = None,
|
||||
parent_folder_id: str = "msgfolderroot",
|
||||
recursive: bool = False,
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""List mail folders from the Exchange mailbox.
|
||||
"""List mail folders from the Exchange mailbox.
|
||||
|
||||
Args:
|
||||
parent_folder_id: Parent folder to list children of.
|
||||
Defaults to "msgfolderroot" (top-level). Can be a
|
||||
distinguished folder name or a raw folder ID.
|
||||
recursive: If True, traverse all subfolders recursively (Deep).
|
||||
If False, only list immediate children (Shallow).
|
||||
Args:
|
||||
parent_folder_id: Parent folder to list children of.
|
||||
Defaults to "msgfolderroot" (top-level). Can be a
|
||||
distinguished folder name or a raw folder ID.
|
||||
recursive: If True, traverse all subfolders recursively (Deep).
|
||||
If False, only list immediate children (Shallow).
|
||||
|
||||
Returns:
|
||||
JSON array of folder objects with: name, id, total_count,
|
||||
unread_count, child_folder_count.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON array of folder objects with: name, id, total_count,
|
||||
unread_count, child_folder_count.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
traversal = "Deep" if recursive else "Shallow"
|
||||
traversal = "Deep" if recursive else "Shallow"
|
||||
|
||||
# Determine parent folder id type
|
||||
# Distinguished folder names are short lowercase strings
|
||||
parent_folder = _folder_id_dict(parent_folder_id)
|
||||
# Determine parent folder id type
|
||||
# Distinguished folder names are short lowercase strings
|
||||
parent_folder = _folder_id_dict(parent_folder_id)
|
||||
|
||||
payload = {
|
||||
"__type": "FindFolderJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "FindFolderRequest:#Exchange",
|
||||
"FolderShape": {
|
||||
"__type": "FolderResponseShape:#Exchange",
|
||||
"BaseShape": "Default",
|
||||
},
|
||||
"ParentFolderIds": [parent_folder],
|
||||
"Traversal": traversal,
|
||||
"Paging": {
|
||||
"__type": "IndexedPageView:#Exchange",
|
||||
"BasePoint": "Beginning",
|
||||
"Offset": 0,
|
||||
"MaxEntriesReturned": 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
payload = {
|
||||
"__type": "FindFolderJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
"Body": {
|
||||
"__type": "FindFolderRequest:#Exchange",
|
||||
"FolderShape": {
|
||||
"__type": "FolderResponseShape:#Exchange",
|
||||
"BaseShape": "Default",
|
||||
},
|
||||
"ParentFolderIds": [parent_folder],
|
||||
"Traversal": traversal,
|
||||
"Paging": {
|
||||
"__type": "IndexedPageView:#Exchange",
|
||||
"BasePoint": "Beginning",
|
||||
"Offset": 0,
|
||||
"MaxEntriesReturned": 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request("FindFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
try:
|
||||
data = client.request("FindFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
folders = []
|
||||
for msg in client.extract_items(data):
|
||||
if "RootFolder" in msg and "Folders" in msg["RootFolder"]:
|
||||
for f in msg["RootFolder"]["Folders"]:
|
||||
folders.append(
|
||||
{
|
||||
"name": f.get("DisplayName", "Unknown"),
|
||||
"id": f.get("FolderId", {}).get("Id", ""),
|
||||
"total_count": f.get("TotalCount", 0),
|
||||
"unread_count": f.get("UnreadCount", 0),
|
||||
"child_folder_count": f.get("ChildFolderCount", 0),
|
||||
}
|
||||
)
|
||||
folders = []
|
||||
for msg in client.extract_items(data):
|
||||
if "RootFolder" in msg and "Folders" in msg["RootFolder"]:
|
||||
for f in msg["RootFolder"]["Folders"]:
|
||||
folders.append(
|
||||
{
|
||||
"name": f.get("DisplayName", "Unknown"),
|
||||
"id": f.get("FolderId", {}).get("Id", ""),
|
||||
"total_count": f.get("TotalCount", 0),
|
||||
"unread_count": f.get("UnreadCount", 0),
|
||||
"child_folder_count": f.get("ChildFolderCount", 0),
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps(folders, ensure_ascii=False)
|
||||
return json.dumps(folders, ensure_ascii=False)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_folder(
|
||||
name: str,
|
||||
parent_folder_id: str = "msgfolderroot",
|
||||
ctx: Context = None,
|
||||
name: str,
|
||||
parent_folder_id: str = "msgfolderroot",
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Create a new mail folder.
|
||||
"""Create a new mail folder.
|
||||
|
||||
Args:
|
||||
name: Display name for the new folder.
|
||||
parent_folder_id: Parent folder to create under.
|
||||
Defaults to "msgfolderroot" (top-level). Can be a
|
||||
distinguished folder name or a raw folder ID.
|
||||
Args:
|
||||
name: Display name for the new folder.
|
||||
parent_folder_id: Parent folder to create under.
|
||||
Defaults to "msgfolderroot" (top-level). Can be a
|
||||
distinguished folder name or a raw folder ID.
|
||||
|
||||
Returns:
|
||||
JSON object with the created folder's id and name.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with the created folder's id and name.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
payload = {
|
||||
"__type": "CreateFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "CreateFolderRequest:#Exchange",
|
||||
"ParentFolderId": {
|
||||
"__type": "TargetFolderId:#Exchange",
|
||||
"BaseFolderId": _folder_id_dict(parent_folder_id),
|
||||
},
|
||||
"Folders": [
|
||||
{
|
||||
"__type": "Folder:#Exchange",
|
||||
"DisplayName": name,
|
||||
"FolderClass": "IPF.Note",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
payload = {
|
||||
"__type": "CreateFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "CreateFolderRequest:#Exchange",
|
||||
"ParentFolderId": {
|
||||
"__type": "TargetFolderId:#Exchange",
|
||||
"BaseFolderId": _folder_id_dict(parent_folder_id),
|
||||
},
|
||||
"Folders": [
|
||||
{
|
||||
"__type": "Folder:#Exchange",
|
||||
"DisplayName": name,
|
||||
"FolderClass": "IPF.Note",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request_header_payload("CreateFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
try:
|
||||
data = client.request_header_payload("CreateFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"name": name,
|
||||
"id": folder.get("FolderId", {}).get("Id", ""),
|
||||
}
|
||||
)
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"name": name,
|
||||
"id": folder.get("FolderId", {}).get("Id", ""),
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def rename_folder(
|
||||
folder_id: str,
|
||||
new_name: str,
|
||||
ctx: Context = None,
|
||||
folder_id: str,
|
||||
new_name: str,
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Rename an existing mail folder.
|
||||
"""Rename an existing mail folder.
|
||||
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to rename (from get_folders).
|
||||
new_name: New display name for the folder.
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to rename (from get_folders).
|
||||
new_name: New display name for the folder.
|
||||
|
||||
Returns:
|
||||
JSON object with success status and new folder id.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with success status and new folder id.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
payload = {
|
||||
"__type": "UpdateFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "UpdateFolderRequest:#Exchange",
|
||||
"FolderChanges": [
|
||||
{
|
||||
"__type": "FolderChange:#Exchange",
|
||||
"FolderId": {
|
||||
"__type": "FolderId:#Exchange",
|
||||
"Id": folder_id,
|
||||
},
|
||||
"Updates": [
|
||||
{
|
||||
"__type": "SetFolderField:#Exchange",
|
||||
"Path": {
|
||||
"__type": "PropertyUri:#Exchange",
|
||||
"FieldURI": "FolderDisplayName",
|
||||
},
|
||||
"Folder": {
|
||||
"__type": "Folder:#Exchange",
|
||||
"DisplayName": new_name,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
payload = {
|
||||
"__type": "UpdateFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "UpdateFolderRequest:#Exchange",
|
||||
"FolderChanges": [
|
||||
{
|
||||
"__type": "FolderChange:#Exchange",
|
||||
"FolderId": {
|
||||
"__type": "FolderId:#Exchange",
|
||||
"Id": folder_id,
|
||||
},
|
||||
"Updates": [
|
||||
{
|
||||
"__type": "SetFolderField:#Exchange",
|
||||
"Path": {
|
||||
"__type": "PropertyUri:#Exchange",
|
||||
"FieldURI": "FolderDisplayName",
|
||||
},
|
||||
"Folder": {
|
||||
"__type": "Folder:#Exchange",
|
||||
"DisplayName": new_name,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request_header_payload("UpdateFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
try:
|
||||
data = client.request_header_payload("UpdateFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"new_name": new_name,
|
||||
"id": folder.get("FolderId", {}).get("Id", ""),
|
||||
}
|
||||
)
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"new_name": new_name,
|
||||
"id": folder.get("FolderId", {}).get("Id", ""),
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def empty_folder(
|
||||
folder_id: str,
|
||||
delete_sub_folders: bool = False,
|
||||
permanent: bool = False,
|
||||
ctx: Context = None,
|
||||
folder_id: str,
|
||||
delete_sub_folders: bool = False,
|
||||
permanent: bool = False,
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Empty all items from a mail folder.
|
||||
"""Empty all items from a mail folder.
|
||||
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to empty (from get_folders).
|
||||
delete_sub_folders: If True, also delete sub-folders. Default False.
|
||||
permanent: If True, permanently delete items (HardDelete).
|
||||
Otherwise move to Deleted Items. Default False.
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to empty (from get_folders).
|
||||
delete_sub_folders: If True, also delete sub-folders. Default False.
|
||||
permanent: If True, permanently delete items (HardDelete).
|
||||
Otherwise move to Deleted Items. Default False.
|
||||
|
||||
Returns:
|
||||
JSON object with success status.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with success status.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
delete_type = "HardDelete" if permanent else "MoveToDeletedItems"
|
||||
delete_type = "HardDelete" if permanent else "MoveToDeletedItems"
|
||||
|
||||
payload = {
|
||||
"__type": "EmptyFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "EmptyFolderRequest:#Exchange",
|
||||
"FolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"DeleteType": delete_type,
|
||||
"DeleteSubFolders": delete_sub_folders,
|
||||
"SuppressReadReceipt": True,
|
||||
},
|
||||
}
|
||||
payload = {
|
||||
"__type": "EmptyFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "EmptyFolderRequest:#Exchange",
|
||||
"FolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"DeleteType": delete_type,
|
||||
"DeleteSubFolders": delete_sub_folders,
|
||||
"SuppressReadReceipt": True,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request_header_payload("EmptyFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
try:
|
||||
data = client.request_header_payload("EmptyFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
for msg in client.extract_items(data):
|
||||
if msg.get("ResponseClass") == "Success":
|
||||
return json.dumps({"success": True, "folder_id": folder_id})
|
||||
for msg in client.extract_items(data):
|
||||
if msg.get("ResponseClass") == "Success":
|
||||
return json.dumps({"success": True, "folder_id": folder_id})
|
||||
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def delete_folder(
|
||||
folder_id: str,
|
||||
permanent: bool = False,
|
||||
ctx: Context = None,
|
||||
folder_id: str,
|
||||
permanent: bool = False,
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Delete a mail folder.
|
||||
"""Delete a mail folder.
|
||||
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to delete (from get_folders).
|
||||
permanent: If True, permanently delete (HardDelete).
|
||||
Otherwise move to Deleted Items. Default False.
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to delete (from get_folders).
|
||||
permanent: If True, permanently delete (HardDelete).
|
||||
Otherwise move to Deleted Items. Default False.
|
||||
|
||||
Returns:
|
||||
JSON object with success status.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with success status.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
delete_type = "HardDelete" if permanent else "MoveToDeletedItems"
|
||||
delete_type = "HardDelete" if permanent else "MoveToDeletedItems"
|
||||
|
||||
payload = {
|
||||
"__type": "DeleteFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "DeleteFolderRequest:#Exchange",
|
||||
"FolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"DeleteType": delete_type,
|
||||
},
|
||||
}
|
||||
payload = {
|
||||
"__type": "DeleteFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "DeleteFolderRequest:#Exchange",
|
||||
"FolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"DeleteType": delete_type,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request_header_payload("DeleteFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
try:
|
||||
data = client.request_header_payload("DeleteFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
for msg in client.extract_items(data):
|
||||
if msg.get("ResponseClass") == "Success":
|
||||
return json.dumps({"success": True, "folder_id": folder_id})
|
||||
for msg in client.extract_items(data):
|
||||
if msg.get("ResponseClass") == "Success":
|
||||
return json.dumps({"success": True, "folder_id": folder_id})
|
||||
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def move_folder(
|
||||
folder_id: str,
|
||||
target_parent_folder_id: str = "msgfolderroot",
|
||||
ctx: Context = None,
|
||||
folder_id: str,
|
||||
target_parent_folder_id: str = "msgfolderroot",
|
||||
ctx: Context = None,
|
||||
) -> str:
|
||||
"""Move a mail folder to a different parent folder.
|
||||
"""Move a mail folder to a different parent folder.
|
||||
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to move (from get_folders).
|
||||
target_parent_folder_id: Destination parent folder.
|
||||
Defaults to "msgfolderroot" (top-level). Can be a
|
||||
distinguished folder name or a raw folder ID.
|
||||
Args:
|
||||
folder_id: The Exchange folder ID to move (from get_folders).
|
||||
target_parent_folder_id: Destination parent folder.
|
||||
Defaults to "msgfolderroot" (top-level). Can be a
|
||||
distinguished folder name or a raw folder ID.
|
||||
|
||||
Returns:
|
||||
JSON object with success status and new folder ID.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON object with success status and new folder ID.
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
payload = {
|
||||
"__type": "MoveFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "MoveFolderRequest:#Exchange",
|
||||
"FolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"ToFolderId": {
|
||||
"__type": "TargetFolderId:#Exchange",
|
||||
"BaseFolderId": _folder_id_dict(target_parent_folder_id),
|
||||
},
|
||||
},
|
||||
}
|
||||
payload = {
|
||||
"__type": "MoveFolderJsonRequest:#Exchange",
|
||||
"Header": _HEADER_TZ,
|
||||
"Body": {
|
||||
"__type": "MoveFolderRequest:#Exchange",
|
||||
"FolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"ToFolderId": {
|
||||
"__type": "TargetFolderId:#Exchange",
|
||||
"BaseFolderId": _folder_id_dict(target_parent_folder_id),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.request_header_payload("MoveFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
try:
|
||||
data = client.request_header_payload("MoveFolder", payload)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"folder_id": folder.get("FolderId", {}).get("Id", ""),
|
||||
}
|
||||
)
|
||||
for msg in client.extract_items(data):
|
||||
if "Folders" in msg:
|
||||
folder = msg["Folders"][0]
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"folder_id": folder.get("FolderId", {}).get("Id", ""),
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
return json.dumps({"error": "Unexpected response", "raw": str(data)})
|
||||
|
||||
+86
-86
@@ -13,109 +13,109 @@ from owa_mcp.server_lifecycle import require_client
|
||||
|
||||
|
||||
def _get_client(ctx: Context) -> OWAClient:
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
app_ctx: AppContext = ctx.request_context.lifespan_context
|
||||
return require_client(app_ctx.client)
|
||||
"""Extract the OWAClient from the MCP lifespan context."""
|
||||
app_ctx: AppContext = ctx.request_context.lifespan_context
|
||||
return require_client(app_ctx.client)
|
||||
|
||||
|
||||
def _parse_person(resolution: dict) -> dict:
|
||||
"""Parse person data from a ResolveNames resolution entry.
|
||||
"""Parse person data from a ResolveNames resolution entry.
|
||||
|
||||
Preserves the exact logic from find-person.py parse_person().
|
||||
"""
|
||||
mailbox = resolution.get("Mailbox", {})
|
||||
contact = resolution.get("Contact", {})
|
||||
Preserves the exact logic from find-person.py parse_person().
|
||||
"""
|
||||
mailbox = resolution.get("Mailbox", {})
|
||||
contact = resolution.get("Contact", {})
|
||||
|
||||
person = {
|
||||
"name": mailbox.get("Name", contact.get("DisplayName", "")),
|
||||
"email": mailbox.get("EmailAddress", ""),
|
||||
"type": mailbox.get("MailboxType", ""),
|
||||
"first_name": contact.get("GivenName", ""),
|
||||
"last_name": contact.get("Surname", ""),
|
||||
"job_title": contact.get("JobTitle", ""),
|
||||
"department": contact.get("Department", ""),
|
||||
"company": contact.get("CompanyName", ""),
|
||||
"office": contact.get("OfficeLocation", ""),
|
||||
"manager": "",
|
||||
"manager_email": "",
|
||||
"phones": {},
|
||||
"address": {},
|
||||
"direct_reports": [],
|
||||
"alias": contact.get("Alias", ""),
|
||||
}
|
||||
person = {
|
||||
"name": mailbox.get("Name", contact.get("DisplayName", "")),
|
||||
"email": mailbox.get("EmailAddress", ""),
|
||||
"type": mailbox.get("MailboxType", ""),
|
||||
"first_name": contact.get("GivenName", ""),
|
||||
"last_name": contact.get("Surname", ""),
|
||||
"job_title": contact.get("JobTitle", ""),
|
||||
"department": contact.get("Department", ""),
|
||||
"company": contact.get("CompanyName", ""),
|
||||
"office": contact.get("OfficeLocation", ""),
|
||||
"manager": "",
|
||||
"manager_email": "",
|
||||
"phones": {},
|
||||
"address": {},
|
||||
"direct_reports": [],
|
||||
"alias": contact.get("Alias", ""),
|
||||
}
|
||||
|
||||
# Phone numbers
|
||||
for phone in contact.get("PhoneNumbers", []):
|
||||
key = phone.get("Key", "")
|
||||
number = phone.get("PhoneNumber", "")
|
||||
if number:
|
||||
person["phones"][key] = number
|
||||
# Phone numbers
|
||||
for phone in contact.get("PhoneNumbers", []):
|
||||
key = phone.get("Key", "")
|
||||
number = phone.get("PhoneNumber", "")
|
||||
if number:
|
||||
person["phones"][key] = number
|
||||
|
||||
# Physical address
|
||||
for addr in contact.get("PhysicalAddresses", []):
|
||||
if addr.get("Key") == "Business":
|
||||
parts = []
|
||||
if addr.get("Street"):
|
||||
parts.append(addr["Street"])
|
||||
if addr.get("City"):
|
||||
parts.append(addr["City"])
|
||||
if addr.get("PostalCode"):
|
||||
parts.append(addr["PostalCode"])
|
||||
if addr.get("CountryOrRegion"):
|
||||
parts.append(addr["CountryOrRegion"])
|
||||
if parts:
|
||||
person["address"] = {
|
||||
"street": addr.get("Street", ""),
|
||||
"city": addr.get("City", ""),
|
||||
"postal_code": addr.get("PostalCode", ""),
|
||||
"country": addr.get("CountryOrRegion", ""),
|
||||
"full": ", ".join(parts),
|
||||
}
|
||||
# Physical address
|
||||
for addr in contact.get("PhysicalAddresses", []):
|
||||
if addr.get("Key") == "Business":
|
||||
parts = []
|
||||
if addr.get("Street"):
|
||||
parts.append(addr["Street"])
|
||||
if addr.get("City"):
|
||||
parts.append(addr["City"])
|
||||
if addr.get("PostalCode"):
|
||||
parts.append(addr["PostalCode"])
|
||||
if addr.get("CountryOrRegion"):
|
||||
parts.append(addr["CountryOrRegion"])
|
||||
if parts:
|
||||
person["address"] = {
|
||||
"street": addr.get("Street", ""),
|
||||
"city": addr.get("City", ""),
|
||||
"postal_code": addr.get("PostalCode", ""),
|
||||
"country": addr.get("CountryOrRegion", ""),
|
||||
"full": ", ".join(parts),
|
||||
}
|
||||
|
||||
# Manager
|
||||
manager_data = contact.get("ManagerMailbox", {}).get("Mailbox", {})
|
||||
if manager_data:
|
||||
person["manager"] = manager_data.get("Name", "")
|
||||
person["manager_email"] = manager_data.get("EmailAddress", "")
|
||||
elif contact.get("Manager"):
|
||||
person["manager"] = contact.get("Manager", "")
|
||||
# Manager
|
||||
manager_data = contact.get("ManagerMailbox", {}).get("Mailbox", {})
|
||||
if manager_data:
|
||||
person["manager"] = manager_data.get("Name", "")
|
||||
person["manager_email"] = manager_data.get("EmailAddress", "")
|
||||
elif contact.get("Manager"):
|
||||
person["manager"] = contact.get("Manager", "")
|
||||
|
||||
# Direct reports
|
||||
for report in contact.get("DirectReports", []):
|
||||
person["direct_reports"].append(
|
||||
{
|
||||
"name": report.get("Name", ""),
|
||||
"email": report.get("EmailAddress", ""),
|
||||
}
|
||||
)
|
||||
# Direct reports
|
||||
for report in contact.get("DirectReports", []):
|
||||
person["direct_reports"].append(
|
||||
{
|
||||
"name": report.get("Name", ""),
|
||||
"email": report.get("EmailAddress", ""),
|
||||
}
|
||||
)
|
||||
|
||||
return person
|
||||
return person
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def find_person(query: str, ctx: Context) -> str:
|
||||
"""Search for people in the corporate directory.
|
||||
"""Search for people in the corporate directory.
|
||||
|
||||
Looks up employees by name, email, department, or keyword using the
|
||||
Exchange ResolveNames API against Active Directory.
|
||||
Looks up employees by name, email, department, or keyword using the
|
||||
Exchange ResolveNames API against Active Directory.
|
||||
|
||||
Args:
|
||||
query: Name, email address, or keyword to search for.
|
||||
Args:
|
||||
query: Name, email address, or keyword to search for.
|
||||
|
||||
Returns:
|
||||
JSON array of matching people with contact details (name, email,
|
||||
job_title, department, company, office, phones, address, manager,
|
||||
direct_reports, alias).
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
Returns:
|
||||
JSON array of matching people with contact details (name, email,
|
||||
job_title, department, company, office, phones, address, manager,
|
||||
direct_reports, alias).
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
try:
|
||||
resolutions = client.resolve_names(query)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
try:
|
||||
resolutions = client.resolve_names(query)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
if not resolutions:
|
||||
return json.dumps([])
|
||||
if not resolutions:
|
||||
return json.dumps([])
|
||||
|
||||
people = [_parse_person(r) for r in resolutions]
|
||||
return json.dumps(people, ensure_ascii=False)
|
||||
people = [_parse_person(r) for r in resolutions]
|
||||
return json.dumps(people, ensure_ascii=False)
|
||||
|
||||
Reference in New Issue
Block a user