ms-graph-search-sdk
Search OneDrive, Teams, and Outlook using Microsoft Graph API with Python. Use when asked to find files, emails, messages, or documents in Microsoft 365 services. This is the preferred method for quick searches.
When & Why to Use This Skill
The Microsoft Graph Search SDK skill empowers Claude to interact directly with the Microsoft 365 ecosystem using Python and the Microsoft Graph API. It provides a secure and efficient way to search across OneDrive, Outlook, and Teams to retrieve files, emails, and messages. With built-in support for timezone handling (Central Time) and a security-first design for token management, this skill streamlines data retrieval from enterprise business platforms while maintaining strict credential privacy.
Use Cases
- File Discovery: Quickly locate specific Excel spreadsheets, PDFs, or presentations across OneDrive and shared document libraries using keyword searches.
- Email Retrieval: Search through Outlook inboxes to find historical project communications, budget approvals, or specific attachments based on sender or subject.
- Meeting Management: List upcoming calendar events and meetings, automatically converting UTC timestamps to Central Time for accurate scheduling.
- Cross-Platform Search: Execute unified queries to find related information simultaneously across emails and cloud storage for comprehensive project overviews.
- Team Collaboration: List joined Microsoft Teams and retrieve group descriptions to navigate organizational structures and shared resources.
| name | ms-graph-search-sdk |
|---|---|
| description | Search OneDrive, Teams, and Outlook using Microsoft Graph API with Python. Use when asked to find files, emails, messages, or documents in Microsoft 365 services. This is the preferred method for quick searches. |
| allowed-tools | [Bash] |
Microsoft Graph Search SDK Skill
When asked to search OneDrive, Teams, Outlook, or other Microsoft 365 services, use Python with the requests library for clean, simple API calls.
🔒 Security-First Design
ONE CRITICAL RULE: Claude NEVER displays your token back to you.
Simple workflow:
- Claude opens Graph Explorer for you
- You paste token in chat (it's fine)
- Claude uses it but never echoes it back
Step 1: Install Dependencies (Auto)
Check and install if needed:
pip3 install -q requests
Step 2: Get Access Token
Simple 3-Step Process:
Claude opens Graph Explorer (automatically)
# macOS: open "https://developer.microsoft.com/en-us/graph/graph-explorer" # Linux: xdg-open "https://developer.microsoft.com/en-us/graph/graph-explorer" # Windows: start "https://developer.microsoft.com/en-us/graph/graph-explorer"You get token from browser:
- Sign in with Microsoft account
- Click "Access token" tab
- Copy the token
- Paste it in chat (it's fine to paste)
Claude uses it silently:
- Stores in Python variable
- Makes API calls
- Never displays it back
Token Notes:
- Expires in 69-90 minutes, just get a new one when needed
- Claude will store it for the session
Step 3: Search Using Python
Simple Token Usage
#!/usr/bin/env python3
import requests
# User pastes token in chat, Claude uses it
TOKEN = "user_provided_token_here" # Claude replaces with actual token
# ✅ CORRECT: Use it silently
headers = {"Authorization": f"Bearer {TOKEN}"}
response = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers)
# ❌ WRONG: NEVER display the token
# print(f"Using token: {TOKEN}") # DON'T DO THIS
Timezone Handling (Central Time)
IMPORTANT: Always convert dates/times to Central Time Zone for display.
from datetime import datetime
from zoneinfo import ZoneInfo
def to_central_time(iso_datetime_str):
"""Convert ISO datetime string to Central Time"""
if not iso_datetime_str or iso_datetime_str == 'Unknown':
return 'Unknown'
# Remove trailing zeros and parse
dt_str = iso_datetime_str.rstrip('0').rstrip('.')
# Parse the datetime (assume UTC if no timezone)
if 'Z' in dt_str or '+' in dt_str or dt_str.endswith('-'):
dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
else:
dt = datetime.fromisoformat(dt_str).replace(tzinfo=ZoneInfo('UTC'))
# Convert to Central Time
central = dt.astimezone(ZoneInfo('America/Chicago'))
# Format nicely
return central.strftime('%Y-%m-%d %I:%M %p CST')
# Example usage:
# start_time = to_central_time("2026-01-06T15:00:00.0000000")
# print(start_time) # "2026-01-06 09:00 AM CST"
Common Operations
All examples below assume:
import os
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
TOKEN = os.getenv('MS_GRAPH_TOKEN')
if not TOKEN:
print("Error: MS_GRAPH_TOKEN not set", file=sys.stderr)
sys.exit(1)
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
Get Current User
response = requests.get("https://graph.microsoft.com/v1.0/me", headers=HEADERS)
user = response.json()
print(f"Name: {user.get('displayName')}")
print(f"Email: {user.get('userPrincipalName')}")
Search OneDrive Files
query = "report"
url = f"https://graph.microsoft.com/v1.0/me/drive/root/search(q='{query}')"
response = requests.get(url, headers=HEADERS)
files = response.json().get('value', [])
for file in files[:10]:
print(f"📄 {file['name']}")
print(f" Modified: {to_central_time(file.get('lastModifiedDateTime', 'Unknown'))}")
print(f" Size: {file.get('size', 0):,} bytes")
List OneDrive Files
response = requests.get("https://graph.microsoft.com/v1.0/me/drive/root/children", headers=HEADERS)
files = response.json().get('value', [])
for file in files[:10]:
print(f"📁 {file['name']}")
print(f" Type: {file.get('@microsoft.graph.downloadUrl', 'Folder' if 'folder' in file else 'File')}")
Files Shared With Me
response = requests.get("https://graph.microsoft.com/v1.0/me/drive/sharedWithMe", headers=HEADERS)
files = response.json().get('value', [])
for item in files[:10]:
remote_item = item.get('remoteItem', {})
shared_by = item.get('shared', {}).get('sharedBy', {}).get('user', {}).get('displayName', 'Unknown')
print(f"📤 {remote_item.get('name', 'Unknown')}")
print(f" Shared by: {shared_by}")
Search Emails
query = "budget"
url = f"https://graph.microsoft.com/v1.0/me/messages?$search=\"{query}\"&$top=10"
response = requests.get(url, headers=HEADERS)
messages = response.json().get('value', [])
for msg in messages:
print(f"✉️ {msg.get('subject', 'No subject')}")
sender = msg.get('from', {}).get('emailAddress', {}).get('address', 'Unknown')
print(f" From: {sender}")
print(f" Date: {to_central_time(msg.get('receivedDateTime', 'Unknown'))}")
List Inbox Messages
url = "https://graph.microsoft.com/v1.0/me/messages?$top=10&$select=subject,from,receivedDateTime"
response = requests.get(url, headers=HEADERS)
messages = response.json().get('value', [])
for msg in messages:
sender = msg.get('from', {}).get('emailAddress', {}).get('name', 'Unknown')
print(f"📧 {msg.get('subject', 'No subject')}")
print(f" From: {sender}")
print(f" Date: {to_central_time(msg.get('receivedDateTime', 'Unknown'))}")
List Teams
response = requests.get("https://graph.microsoft.com/v1.0/me/joinedTeams", headers=HEADERS)
teams = response.json().get('value', [])
for team in teams:
print(f"👥 {team.get('displayName', 'Unknown')}")
print(f" Description: {team.get('description', 'No description')}")
List Calendar Events
response = requests.get("https://graph.microsoft.com/v1.0/me/calendar/events?$top=10", headers=HEADERS)
events = response.json().get('value', [])
for event in events:
# Get start/end times and convert to Central Time
start = event.get('start', {}).get('dateTime', 'Unknown')
end = event.get('end', {}).get('dateTime', 'Unknown')
print(f"📅 {event.get('subject', 'No subject')}")
print(f" Start: {to_central_time(start)}")
print(f" End: {to_central_time(end)}")
print(f" Location: {event.get('location', {}).get('displayName', 'No location')}")
Unified Search (Multiple Services)
search_body = {
"requests": [{
"entityTypes": ["driveItem", "message"],
"query": {"queryString": "quarterly report"},
"from": 0,
"size": 25
}]
}
response = requests.post(
"https://graph.microsoft.com/v1.0/search/query",
headers={**HEADERS, "Content-Type": "application/json"},
json=search_body
)
results = response.json()
# Parse results
for request_result in results.get('value', []):
for hit_container in request_result.get('hitsContainers', []):
for hit in hit_container.get('hits', []):
resource = hit.get('resource', {})
print(f"🔍 {resource.get('name') or resource.get('subject', 'Unknown')}")
print(f" Type: {resource.get('@odata.type', 'Unknown')}")
Error Handling Template
def handle_response(response, reopen_browser=False):
"""Handle API response with proper error messages"""
if response.status_code == 401:
print("❌ Token expired or invalid", file=sys.stderr)
print("", file=sys.stderr)
print("To get a new token:", file=sys.stderr)
print("1. Open https://developer.microsoft.com/en-us/graph/graph-explorer", file=sys.stderr)
print("2. Sign in and click 'Access token' tab", file=sys.stderr)
print("3. Run: export MS_GRAPH_TOKEN=\"<your-token>\"", file=sys.stderr)
if reopen_browser:
import platform
import subprocess
system = platform.system()
url = "https://developer.microsoft.com/en-us/graph/graph-explorer"
try:
if system == "Darwin":
subprocess.run(["open", url])
elif system == "Linux":
subprocess.run(["xdg-open", url])
elif system == "Windows":
subprocess.run(["start", url], shell=True)
print("\n✅ Opened Graph Explorer in browser", file=sys.stderr)
except Exception as e:
print(f"\n⚠️ Could not open browser: {e}", file=sys.stderr)
return None
elif response.status_code == 403:
print("❌ Permission denied. Grant consent in Graph Explorer.", file=sys.stderr)
return None
elif response.status_code == 429:
print("❌ Rate limited. Wait a moment and try again.", file=sys.stderr)
return None
elif response.status_code == 200:
return response.json()
else:
print(f"❌ Error {response.status_code}: {response.text}", file=sys.stderr)
return None
# Usage:
# data = handle_response(response, reopen_browser=True)
# if data:
# # Process data
Complete Example Script Template
#!/usr/bin/env python3
"""
Microsoft Graph Search - Simple & Secure
Searches OneDrive and Outlook using Microsoft Graph API.
Security:
- User pastes token in chat (it's fine)
- Claude uses it but never displays it back
"""
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
# User pastes token in chat, Claude replaces this
TOKEN = "user_provided_token_here"
def to_central_time(iso_datetime_str):
"""Convert ISO datetime string to Central Time"""
if not iso_datetime_str or iso_datetime_str == 'Unknown':
return 'Unknown'
dt_str = iso_datetime_str.rstrip('0').rstrip('.')
if 'Z' in dt_str or '+' in dt_str or dt_str.endswith('-'):
dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
else:
dt = datetime.fromisoformat(dt_str).replace(tzinfo=ZoneInfo('UTC'))
central = dt.astimezone(ZoneInfo('America/Chicago'))
return central.strftime('%Y-%m-%d %I:%M %p CST')
def handle_response(response):
"""Handle API response with error checking"""
if response.status_code == 401:
print("❌ Token expired. Get new token from Graph Explorer.")
return None
elif response.status_code == 403:
print("❌ Permission denied.")
return None
elif response.status_code == 429:
print("❌ Rate limited. Wait and retry.")
return None
elif response.status_code == 200:
return response.json()
else:
print(f"❌ Error {response.status_code}: {response.text}")
return None
def search_onedrive(query):
"""Search OneDrive files"""
headers = {"Authorization": f"Bearer {TOKEN}"}
url = f"https://graph.microsoft.com/v1.0/me/drive/root/search(q='{query}')"
response = requests.get(url, headers=headers)
data = handle_response(response)
if data:
files = data.get('value', [])
print(f"\n📁 Found {len(files)} files matching '{query}':\n")
for i, file in enumerate(files[:10], 1):
print(f"{i}. {file['name']}")
print(f" Modified: {to_central_time(file.get('lastModifiedDateTime', 'Unknown'))}")
print(f" Size: {file.get('size', 0):,} bytes\n")
def search_emails(query):
"""Search Outlook emails"""
headers = {"Authorization": f"Bearer {TOKEN}"}
url = f"https://graph.microsoft.com/v1.0/me/messages?$search=\"{query}\"&$top=10"
response = requests.get(url, headers=headers)
data = handle_response(response)
if data:
messages = data.get('value', [])
print(f"\n✉️ Found {len(messages)} emails matching '{query}':\n")
for i, msg in enumerate(messages, 1):
sender = msg.get('from', {}).get('emailAddress', {}).get('address', 'Unknown')
print(f"{i}. {msg.get('subject', 'No subject')}")
print(f" From: {sender}")
print(f" Date: {to_central_time(msg.get('receivedDateTime', 'Unknown'))}\n")
# Run searches
search_onedrive("report")
search_emails("budget")
Output Format
Present results clearly with dates/times in Central Time:
📁 OneDrive Search Results for "quarterly report"
Found 3 files:
1. Q4-2024-Report.xlsx
Modified: 2024-12-15 04:30 AM CST
Size: 45,231 bytes
Path: /Documents/Reports
2. Quarterly-Summary.docx
Modified: 2024-12-10 08:22 AM CST
Size: 23,450 bytes
Path: /Documents/Reports
3. Q4-Presentation.pptx
Modified: 2024-12-05 03:15 AM CST
Size: 1,234,567 bytes
Path: /Shared/Team
Common Queries & Workflows
"Find my Excel files in OneDrive"
- Check if
MS_GRAPH_TOKENis set - If not, open Graph Explorer and instruct user
- Run search:
q='.xlsx' - Display results with file names, dates, sizes
"Search my emails for X"
- Check token (reuse if already set)
- Use
$search="X"parameter - Display subject, sender, date in Central Time
"Show files shared with me"
- Use
/me/drive/sharedWithMeendpoint - Parse remoteItem for file details
- Show who shared each file
"What meetings do I have today?"
- Use
/me/calendar/events - Filter by date
- Convert times to Central Time using
to_central_time() - Display subject, time, location
Best Practices
Security
- Always read token from
MS_GRAPH_TOKENenvironment variable - Never pass token as command-line argument
- Never print token to stdout or stderr
- Never log token to files
- Store token in memory only during script execution
API Usage
- Use
$topparameter to limit results (avoid large responses) - Use
$selectto request only needed fields - Handle pagination for large result sets
- Implement exponential backoff for rate limits
Time Handling
- Always convert dates/times to Central Time for display
- Use the
to_central_time()function consistently - Handle missing or invalid datetime strings gracefully
Error Handling
- Check response status codes before parsing JSON
- Provide helpful error messages to stderr
- Re-open browser on 401 errors (expired token)
- Exit with non-zero code on errors
Code Quality
- Use descriptive function names
- Add docstrings to functions
- Pretty-print JSON with
json.dumps(data, indent=2)when debugging - Use f-strings for formatting
Platform Detection for Browser Opening
import platform
import subprocess
def open_graph_explorer():
"""Open Graph Explorer in default browser"""
system = platform.system()
url = "https://developer.microsoft.com/en-us/graph/graph-explorer"
try:
if system == "Darwin": # macOS
subprocess.run(["open", url])
elif system == "Linux":
subprocess.run(["xdg-open", url])
elif system == "Windows":
subprocess.run(["start", url], shell=True)
print("✅ Opened Graph Explorer in browser")
except Exception as e:
print(f"⚠️ Could not open browser: {e}", file=sys.stderr)
print(f"Please open manually: {url}", file=sys.stderr)
Advantages Over curl
- Cleaner code - No complex escaping or quoting
- Better error handling - Native Python exceptions
- Easier parsing - Direct dict access vs JSON strings
- Token security - Claude never displays it back
- Type safety - Python type hints and linting
- Maintainability - Easier to read and modify
Troubleshooting
Token Expired
❌ Token expired or invalid
Solution:
- Get new token from Graph Explorer (token expires after 69-90 minutes)
- Paste new token in chat
Permission Denied
❌ Permission denied
Solution:
- In Graph Explorer, grant consent for required scopes
- Check Permissions Reference
Module Not Found
ModuleNotFoundError: No module named 'requests'
Solution:
pip3 install requests
Timezone Issues
ZoneInfoNotFoundError: 'America/Chicago'
Solution:
# Install tzdata
pip3 install tzdata
Resources
Microsoft Graph
| Resource | Link |
|---|---|
| Graph Explorer | https://developer.microsoft.com/en-us/graph/graph-explorer |
| API Reference | https://learn.microsoft.com/en-us/graph/api/overview |
| Permissions Reference | https://learn.microsoft.com/en-us/graph/permissions-reference |
| Quick Start Guide | https://learn.microsoft.com/en-us/graph/tutorials |
Python Libraries
- requests: https://requests.readthedocs.io/
- datetime: https://docs.python.org/3/library/datetime.html
- zoneinfo: https://docs.python.org/3/library/zoneinfo.html
Important Notes
- Simple approach - Uses requests library for direct API calls
- Token flow - You paste token in chat (it's fine), Claude never displays it back
- Screen protection - Token never echoed or printed
- Token reuse - Claude stores it in memory for the session
- Auto-install - Claude Code can install requests automatically
- Graph Explorer - Uses Graph Explorer for token generation
- Central Time - All dates/times converted to America/Chicago timezone
Last Updated: 2026-01-11
Security Notice: The ONE rule: Claude never displays your token back to you. Paste it in chat, Claude uses it silently.