| name | sharepoint-video |
| description | Download videos from SharePoint/OneDrive/Teams meeting recordings using Chrome DevTools MCP for authentication. Triggers on SharePoint video URLs, Teams recordings, OneDrive video links. |
| allowed-tools | ["Bash","Read","Write","Glob","Grep","Edit","ToolSearch","mcp__chrome-devtools__list_pages","mcp__chrome-devtools__select_page","mcp__chrome-devtools__navigate_page","mcp__chrome-devtools__take_screenshot","mcp__chrome-devtools__evaluate_script","mcp__chrome-devtools__list_network_requests","mcp__chrome-devtools__get_network_request","mcp__chrome-devtools__take_snapshot"] |
SharePoint Video Downloader
Download videos from SharePoint, OneDrive, and Teams meeting recordings that are otherwise restricted to browser-only playback.
When To Use
- User shares a SharePoint video URL (e.g.,
sharepoint.com/...stream.aspx?id=...)
- User wants to download a Teams meeting recording
- User shares a OneDrive video link
- Any Microsoft 365 hosted video that can't be downloaded directly
Triggers
- "download this sharepoint video"
- "download this teams recording"
- "save this video from sharepoint"
- Any URL containing
sharepoint.com and video-related paths (stream.aspx, Recordings/, .mp4)
Prerequisites
- Chrome DevTools MCP must be configured and available
- ffmpeg must be installed (
brew install ffmpeg)
- User must be able to log into the SharePoint/Microsoft 365 account in the Chrome DevTools browser
The Technique
SharePoint videos use DASH streaming with encrypted segments. Direct download APIs return 403 (view-only shares) or encrypted data. The key insight: the browser already has authenticated access via httpOnly cookies that JavaScript can't read, but Chrome DevTools Protocol network inspection can.
Why Other Approaches Fail
| Approach | Why it fails |
|---|
Direct download API ($value endpoint) | Returns 403 for view-only shares |
| download.aspx | AccessDenied redirect for view-only |
| ffmpeg with manifest URL (no cookies) | HTTP 500 - no auth |
| Fetching segments via browser JS | Content is DRM-encrypted (fmp4 with enableEncryption=1) |
document.cookie / cookieStore.getAll() | Only returns JS-accessible cookies, not httpOnly auth cookies |
| yt-dlp | Doesn't support SharePoint Stream authentication natively |
Why This Approach Works
Path A (Unencrypted): Some videos serve unencrypted DASH streams. The videomanifest URL with &format=dash serves a DASH manifest that ffmpeg can consume directly with cookies. ffmpeg downloads the unencrypted transcoded stream in one shot.
Path B (SEA-Encrypted): Videos with enableEncryption=1 in the DASH manifest use SharePoint's SEA (Segment Encryption Architecture) with AES-128-CBC. ffmpeg's DASH demuxer fails because it only supports standard CENC, not sea:aes128-cbc. The solution:
- Extract the decryption key from
window.g_streamBootstrapContent.dashConfig.cdnDecryptionKey in the browser
- Download each encrypted segment with curl/fetch using cookies
- Decrypt with AES-128-CBC (full segment, PKCS7 padding)
- Concatenate decrypted init + media segments per track
- Mux video + audio tracks with ffmpeg
How to detect: Download the DASH manifest with curl+cookies. If it contains <sea:SegmentEncryption schemeIdUri="urn:mpeg:dash:sea:aes128-cbc:2013">, use Path B. If no ContentProtection elements, use Path A.
Step-by-Step Process
Step 1: Load Chrome DevTools MCP
ToolSearch: query="chrome-devtools navigate"
This loads the Chrome DevTools tools. If the MCP fails to connect:
- Check if another Chrome instance is using the debug profile:
pkill -f "chrome-devtools-mcp/chrome-profile"
- Remove stale lock:
rm -f ~/Library/Application\ Support/chrome-devtools-mcp/chrome-profile/SingletonLock
Step 2: Navigate to the Video
mcp__chrome-devtools__navigate_page({ url: "<sharepoint-video-url>" })
If the page redirects to a Microsoft login page, tell the user to log in manually. Take a screenshot to verify the state:
mcp__chrome-devtools__take_screenshot()
After login, the user may need to indicate which tab has the video. Use list_pages and select_page to switch to the correct tab.
Step 3: Wait for Video to Load
Take a screenshot to confirm the video player is visible and the page has fully loaded. The video must start playing (even briefly) so that the network requests for the videomanifest and transcode segments are captured.
Step 4: Extract Cookies from Network Requests
List fetch/XHR requests to find one that went to the SharePoint domain:
mcp__chrome-devtools__list_network_requests({ resourceTypes: ["fetch", "xhr"], pageSize: 20 })
Look for a request to accessinfinityuk-my.sharepoint.com (or whatever the tenant domain is). Any authenticated SharePoint API request will have the cookies in its request headers.
Get the full request details to extract cookie values:
mcp__chrome-devtools__get_network_request({ reqid: <request-id> })
From the request headers, extract the cookie: header which contains:
rtFa - SharePoint refresh token cookie (httpOnly)
FedAuth - SharePoint federation auth cookie (httpOnly)
SIMI - Session cookie
These are the critical auth cookies that document.cookie cannot access.
Step 5: Get the Videomanifest URL
From the same network request listing (Step 4), find the request to svc.ms/transform/videomanifest. This URL contains:
provider=spo
docid= (the document identifier with tempauth token)
format=dash
The manifest URL is long. Copy it up to and including &format=dash. Remove any &cTag= parameter if present (it can cause issues).
Alternatively, the manifest URL may be in the list_network_requests output directly.
Step 6: Download with ffmpeg (Path A - Unencrypted)
Write the cookies to ffmpeg's -cookies flag format and download:
ffmpeg -y -cookies "rtFa=<value>; domain=.sharepoint.com; path=/
FedAuth=<value>; domain=.sharepoint.com; path=/
" -i "<manifest-url>" -c copy /tmp/sharepoint-video.mp4
Critical formatting notes:
- Each cookie must be on its own line within the quotes
- Each line ends with
; domain=.sharepoint.com; path=/
- Lines are separated by literal newlines (not
\n)
- The
-c copy flag avoids re-encoding (fast, lossless)
If this fails with "Error when loading first fragment of playlist", the video uses SEA encryption. Use Step 6B instead.
Step 6B: Download with Decryption (Path B - SEA-Encrypted)
6B.1: Check for encryption in the DASH manifest
Download the manifest with curl:
curl -s -b "rtFa=<value>; FedAuth=<value>" "<manifest-url>" > /tmp/sp-manifest.xml
If it contains enableEncryption=1 and sea:aes128-cbc, proceed with decryption.
6B.2: Extract the decryption key from the browser
() => {
const dk = window.g_streamBootstrapContent.dashConfig.cdnDecryptionKey;
const toHex = (obj) => {
let hex = '';
for (let i = 0; i < 16; i++) {
hex += ('0' + obj[i].toString(16)).slice(-2);
}
return hex;
};
return JSON.stringify({
keyHex: toHex(dk.keyBuffer),
ivHex: toHex(dk.iv),
valid: dk.valid
});
}
This returns the AES-128 key and IV as hex strings. The key is loaded inline by the Shaka player (console log: "CDNAdapter - setupFilters: Key and iv provided inline").
6B.3: Parse the manifest for segment URLs
From the DASH manifest XML, extract:
<BaseURL> - the base URL for all segment requests
<SegmentTemplate> initialization attribute - init segment URL template
<SegmentTemplate> media attribute - media segment URL template
<SegmentTimeline> <S d="X" r="Y" /> - segment durations (d) and repeat counts (r)
$RepresentationID$ is replaced with the quality ID (e.g., "vcopy" for video, "audcopy" for audio)
$Time$ is replaced with cumulative segment time
6B.4: Download and decrypt with Python
Use a Python script (pip install cryptography):
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
import urllib.request
KEY = bytes.fromhex("<key_hex>")
IV = bytes.fromhex("<iv_hex>")
def decrypt_segment(data):
cipher = Cipher(algorithms.AES(KEY), modes.CBC(IV), backend=default_backend())
decryptor = cipher.decryptor()
decrypted = decryptor.update(data) + decryptor.finalize()
try:
unpadder = padding.PKCS7(128).unpadder()
decrypted = unpadder.update(decrypted) + unpadder.finalize()
except Exception:
pass
return decrypted
def fetch(url, cookie_header):
req = urllib.request.Request(url)
req.add_header("Cookie", cookie_header)
req.add_header("User-Agent", "Mozilla/5.0")
return urllib.request.urlopen(req, timeout=30).read()
The segment URL pattern: {BaseURL}{SegmentTemplate} with $RepresentationID$ and $Time$ replaced.
Get a working segment URL from get_network_request (find a reqid with oneDrive.transcode in list_network_requests) and use all its query parameters as the template. Key params: correlationid, cs, psi, PlaybackSessionData, headerOffset, headerSize.
6B.5: Verify decryption
Test with just the init segment first. Decrypted fmp4 should start with bytes like 00 00 00 XX 66 74 79 70 (ftyp box). If you see garbage, the key/IV extraction failed.
Step 7: Verify and Move
Verify the download with ffprobe:
ffprobe /tmp/sharepoint-video.mp4 2>&1 | head -20
Check for:
- Valid duration (should match the video length)
- Video stream (h264, correct resolution)
- Audio stream (aac)
Copy to the destination folder specified by the user.
Extracting Transcripts (Bonus)
If the video has a transcript panel (common for Teams recordings), you can extract it from the React fiber tree:
const focusZone = document.querySelector('[class*="focusZoneWithAutoScroll"]');
const fiberKey = Object.keys(focusZone).find(k => k.startsWith('__reactFiber'));
let fiber = focusZone[fiberKey];
let depth = 0;
while (fiber && depth < 50) {
if (fiber.memoizedProps && fiber.memoizedProps.entries && fiber.memoizedProps.entries.length > 10) {
break;
}
fiber = fiber.return;
depth++;
}
This bypasses the virtualized rendering that only shows ~30 items at a time.
Troubleshooting
| Problem | Solution |
|---|
| ffmpeg returns HTTP 500 | Cookies may be expired. Re-extract from a fresh network request. |
| ffmpeg returns HTTP 403 | tempauth token in the manifest URL expired. Reload the page and get a fresh manifest URL. |
| File downloads but is 0 bytes | Manifest URL is malformed. Check for encoding issues in the URL. |
| File downloads but won't play | May have gotten encrypted segments. Check manifest for sea:aes128-cbc. If present, use Path B (Step 6B). |
| ffmpeg "Error when loading first fragment" | Video uses SEA encryption. ffmpeg's DASH demuxer doesn't support sea:aes128-cbc. Use Path B (Step 6B). |
g_streamBootstrapContent is undefined | Video page hasn't fully loaded. Wait for the player to initialize. |
cdnDecryptionKey not in dashConfig | The video may not be encrypted, or the key is fetched lazily. Play the video first to trigger key loading. |
Chrome DevTools expression param ignored | Known quirk: only the function parameter executes. Always use arrow functions like () => { ... }. |
| Chrome DevTools MCP won't connect | Kill stale processes and remove SingletonLock (see Step 1). |
| No videomanifest in network requests | The video hasn't started playing. Click play on the video, wait a few seconds, then check again. |
| Cookies not in request headers | The DevTools MCP may not expose full headers for all requests. Try a different reqid. POST requests to SharePoint APIs reliably include cookies. |
Token Expiry
- tempauth in the manifest URL: expires in ~1 hour from page load
- FedAuth/rtFa cookies: expire in ~5 days
- If download fails midway, reload the SharePoint page and get fresh manifest URL + cookies
Limitations
- Requires the user to manually log into Microsoft 365 in the Chrome DevTools browser
- Only works while the session cookies are valid
- Cannot bypass organizational DRM policies that prevent all playback
- Download speed depends on SharePoint's transcoding/serving speed (typically 1-3 MB/s)
- SEA-encrypted videos (Path B) require downloading ~300-600+ individual segments - slower than Path A but reliable
- Python
cryptography package required for Path B (pip install cryptography)