Skip to main content

Download Microsoft Teams transcripts, chat, and meeting metadata from the macOS desktop app

Project description

Teams Transcript Downloader

A command-line tool that downloads Microsoft Teams meeting transcripts, chat messages, and participant metadata as WebVTT (.vtt) or structured JSON. It connects to the Teams desktop app via the Chrome DevTools Protocol (CDP) and navigates the UI programmatically. An LLM agent (Claude on Vertex AI) handles bounded non-deterministic steps such as selecting ambiguous search results, selecting recurring-meeting occurrences, and disambiguating iframe targets.

Run with uvx

On macOS with uv installed, run the app directly from PyPI without a persistent installation:

uvx teams-transcripts --help
uvx teams-transcripts \
  --meeting "Weekly Standup" \
  --datetime 2026-07-11 \
  --gcp-project your-project

The MCP server has a different executable name from the PyPI package, so use --from to tell uvx which package provides it:

uvx --from 'teams-transcripts==0.5.2' teams-transcripts-mcp
uvx --from teams-transcripts transcript-download --help

Use teams-transcripts==0.5.2 for a reproducible MCP configuration, or replace it with teams-transcripts to follow the latest release. Do not run uvx teams-transcripts-mcp: that asks PyPI for a package named teams-transcripts-mcp, rather than running the executable supplied by this package.

uvx creates an isolated temporary environment. The Teams desktop app, CDP, and authentication prerequisites below still apply.

How it works

The script executes an 8-step pipeline:

Step Description
1 Verify Teams is running with CDP enabled; relaunch if needed
2 Confirm CDP is responding on the configured port
3 Discover Teams page targets via CDP
3b (when --tenant specified) Verify tenant; switch if needed
3c (when --meeting-id used) Resolve UUID to meeting subject and date via MCPS API
4 Type the meeting name into the Teams search bar and navigate to the chat
5 Open Recap when needed, select the correct date occurrence for recurring meetings, extract speaker names from the Speakers sub-tab, then switch to the Transcript tab only when transcript output is requested
6 Locate the transcript iframe target when transcript output is requested
7 Download the transcript when requested, extract roster members and output chat messages when requested, read Event/Call messages and RSVP tracking when needed for metadata, build categorised participant lists, then build VTT or structured JSON
8 Validate generated transcript content when transcript output is requested

Download approaches

When transcript output is requested, the script detects which approach to use based on the Download button state:

Approach A -- Native blob interception is used when you have download permission. The script clicks the Download button and intercepts the blob URL to capture the VTT content byte-for-byte.

Approach B -- React fibre tree extraction is used when download is blocked (the button shows "You don't have permission"). The script walks the React component tree to extract all transcript entries directly from the in-memory data structure, then generates a well-formed VTT file.

Speaker name resolution

Speaker names are resolved using the Recap Speakers tab. The script extracts a GUID-to-display-name mapping and applies it to clean up:

  • Vendor-prefixed names: v-John Doe (Metosin) becomes John Doe
  • Suffixed names: Jane Smith GRM becomes Jane Smith
  • Synthetic placeholders: @1 becomes Speaker 1

Names that are already clean (e.g. internal staff) are left unchanged.

Meeting participant data

The tool extracts categorised participant information from four sources:

Category Source Description
Organiser Roster popover (fibre tree) The meeting creator, identified by organizerId from the chat topic menu
Invited Roster popover (fibre tree) All members of the meeting chat thread, with names, emails, and Azure AD object IDs
Speakers Recap Speakers tab Members who spoke during the meeting, cross-referenced with the roster by display name
Attendees Event/Call <partlist> XML in chat messages Members who joined the call, cross-referenced with the roster by Azure AD object ID

For the invited category, RSVP tracking data can optionally be enriched from the meeting Details tab. The tool navigates to Details, opens the Tracking panel, clicks "Show more attendees" if present, and parses the aria-labels to extract each invitee's calendar response status (accepted, tentative, declined, or no response). This enrichment is soft-failure: if the Tracking panel is unavailable, the pipeline continues without RSVP data.

The roster popover is accessed by clicking the participant count button in the chat header. This retrieves the full member list (names, emails, object IDs) for all members, regardless of meeting size. The legacy membersLimited property in the fibre tree is intentionally limited to ~3 members by Teams and is only used as a fallback.

Prerequisites

1. Microsoft Teams desktop app (macOS)

Teams must be running with CDP remote debugging enabled. If Teams is already running without CDP, the script will kill and relaunch it automatically with the full set of required flags.

To launch manually, only the CDP port is strictly required:

export WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS='--remote-debugging-port=9333'
open -a 'Microsoft Teams'

When the script relaunches Teams itself, it sets additional Chromium flags for reliable automation:

Flag Purpose
--disable-backgrounding-occluded-windows Keep rendering when Teams is behind other windows
--disable-renderer-backgrounding Keep renderer at full priority when unfocused
--disable-hang-monitor Suppress "page unresponsive" dialogs
--disable-ipc-flooding-protection Prevent throttling of rapid CDP commands
--disable-smooth-scrolling Remove animation delays
--run-all-compositor-stages-before-draw Ensure fully composited frames for screenshots
--hide-scrollbars Cleaner screenshots
--mute-audio Suppress notification and meeting sounds
--silent-debugger-extension-api Hide "debugging" infobars
--force-device-scale-factor=2 Retina-quality screenshots for the LLM agent

2. Google Cloud credentials

The LLM agent uses Claude via Vertex AI, so you need Application Default Credentials (ADC) for a GCP project with the Vertex AI API enabled:

gcloud auth application-default login

Or point the --adc flag (or GOOGLE_ADC env var) at a service account key file.

3. Python dependencies

The package requires Python 3.10+ and is managed with uv. When using uvx, dependencies are installed automatically in an isolated environment; no project checkout or uv sync is needed.

For development from a source checkout, install the locked dependencies:

uv sync --locked

This creates a virtual environment in .venv/ and installs the locked dependencies from uv.lock.

Usage

Download a transcript

teams-transcripts (--meeting NAME | --meeting-id UUID) [--datetime YYYY-MM-DD[THH:MM]] [--output PATH] [options]

Or with uv run (no wrapper script needed):

uv run teams-transcripts (--meeting NAME | --meeting-id UUID) [--datetime YYYY-MM-DD[THH:MM]] [--output PATH] [options]

Or via the Python module:

uv run python -m teams_transcripts (--meeting NAME | --meeting-id UUID) [--datetime YYYY-MM-DD[THH:MM]] [--output PATH] [options]

List available tenants

teams-transcripts --list-tenants [--format json] [--output PATH]

Lists all Azure AD tenants the signed-in user can access, marks the current tenant with *, then exits. Use the name or domain value from the output with --tenant in a subsequent download invocation.

List meetings with transcripts

teams-transcripts --list-transcripts [DAYS] [--format json] [--output PATH] [--tenant DOMAIN]

Lists all meetings with transcripts that the signed-in user attended. DAYS must be 1 or greater. This queries the Teams MCPS (Meeting Content and Presence Service) API via CDP worker injection -- no Graph API permissions or app registrations required.

The optional DAYS argument controls how many days back to search (default: 1 = today only).

Multi-tenant behaviour: when --tenant is specified, only the matching tenant is queried. Without --tenant, all signed-in tenants are queried and results aggregated into a single output.

Examples:

# Today's meetings with transcripts (all tenants)
teams-transcripts --list-transcripts

# Last 7 days
teams-transcripts --list-transcripts 7

# Last 14 days, specific tenant, JSON output to file
teams-transcripts --list-transcripts 14 --tenant choreograph.com --format json --output meetings.json

# Last 30 days, table output
teams-transcripts --list-transcripts 30

Text output:

Date        Subject                                           T  R  ID                                    Call ID
----------  ------------------------------------------------  -  -  ------------------------------------  ------------------------------------
2026-04-29  Campaign Connect Leads                            Y  Y  f0a4b1c3-228f-4f24-8efb-6736215a3072  aae0280e-5b7e-473d-8a4b-99de3cf74a08
2026-04-29  OMS Forum                                         Y  Y  3f9c903d-339a-4be7-a796-c13001c6af01  b12f445a-6c1e-4e2d-9f01-4e8a6b7c9d0e
2026-04-29  Data connectors development                       Y  Y  6d5d5a9f-357a-4f3c-a2be-d489454f6762  c23e556b-7d2f-5f3e-a012-5f9b7c8d0e1f
2026-04-29  UAP mid-Weekly                                    Y  Y  39158372-e829-454e-ad92-3d8812527aa9  d34f667c-8e3g-6g4f-b123-6g0c8d9e1f2g
2026-04-29  Dive into data for Data Connectors                Y  Y  67c13927-9b23-4f30-ae67-a489b3842994  e45g778d-9f4h-7h5g-c234-7h1d9e0f2g3h
2026-04-29  UAP / Trafficking / architecture                  Y  Y  589a1e41-3361-4231-8fc0-582e42fe87c3  f56h889e-0g5i-8i6h-d345-8i2e0f1g3h4i
2026-04-28  Optimization Tech leads Fortnightly Sync session  Y  Y  2b4c1f5c-636d-4d78-a087-c2cbf3887fd8  g67i990f-1h6j-9j7i-e456-9j3f1g2h4i5j
2026-04-27  Campaign Connect Leads                            Y  Y  f0a4b1c3-228f-4f24-8efb-6736215a3072  h78j001g-2i7k-0k8j-f567-0k4g2h3i5j6k

8 meeting(s) found.

Columns: Date (ISO), Subject (meeting title, max 48 chars), T (has transcript: Y/N), R (has recording: Y/N), ID (meeting UUID -- use with --meeting-id to download), Call ID (per-occurrence identifier -- use with --call-id for unambiguous targeting of recurring meetings; shown only when MCPS returns call IDs).

Note: for recurring meetings (e.g. "Campaign Connect Leads" above), the ID column shows the same UUID for all occurrences (they share a single chat thread). The Call ID column uniquely identifies each occurrence.

JSON output (--format json):

[
  {
    "subject": "Campaign Connect Leads",
    "date": "2026-04-29",
    "thread_id": "19:meeting_ZjBhNGIxYzMtMjI4Zi00ZjI0LThlZmItNjczNjIxNWEzMDcy@thread.v2",
    "call_id": null,
    "ical_uid": "040000008200E00074C5B7101A82E008...",
    "has_transcript": true,
    "has_recording": true,
    "transcript_url": "https://tenant-my.sharepoint.com/personal/...transcripts/.../content",
    "recording_url": "https://tenant-my.sharepoint.com/personal/.../Meeting%20Recording.mp4"
  }
]

The JSON output includes SharePoint URLs for transcripts/recordings when MCPS returns them. These URLs can be used for direct download with appropriate authentication.

The legacy entry point transcript-download (backed by the original monolith in legacy/transcript_download.py) is still registered but is no longer the primary interface.

Required arguments (download mode)

One of the following is required (mutually exclusive):

Argument Description
--meeting NAME Meeting title as it appears in Teams search results
--meeting-id UUID Meeting UUID from --list-transcripts ID column. Identifies a meeting thread. For non-recurring meetings this is unique; for recurring meetings all occurrences share the same UUID. Use --call-id instead for unambiguous recurring-meeting targeting. Incompatible with --meeting, --call-id, and --datetime.
--call-id CALL_ID Call identifier from --list-transcripts Call ID column. Uniquely identifies a specific occurrence of a meeting, including recurring meetings. The tool resolves the call ID to a meeting subject, date, and thread via the MCPS API. Incompatible with --meeting, --meeting-id, and --datetime.

Optional arguments

Argument Default Description
--datetime YYYY-MM-DD[THH:MM] today Target meeting date, optionally with time. Defaults to today's date when omitted. Date only selects the first occurrence on that date. Adding a time (e.g. 2026-04-22T14:00) disambiguates when there are multiple meetings with the same name on the same day. Incompatible with --meeting-id and --call-id (date is resolved automatically).
--output PATH stdout Output file path. When omitted, content is written to stdout. File format is determined by --format.
--format {vtt,json} vtt Output format. vtt writes WebVTT content. json writes structured JSON with a transcript array of cue objects. Both write to --output (file) or stdout.
--dry-run off Find the meeting, load Recap only when transcript or metadata was requested, detect parts, extract safe roster metadata when metadata or chat was requested, then exit without downloading transcripts, extracting chat, or opening Details tracking. Incompatible with --output.
--quiet / -q off Suppress all diagnostic output on stderr. Errors and --format json output are still emitted.
--verbose / -v off Emit additional diagnostic detail on stderr. Mutually exclusive with --quiet.
--gcp-project PROJECT_ID $GOOGLE_CLOUD_PROJECT GCP project for Vertex AI
--port PORT 9333 CDP remote debugging port. Must be between 1 and 65535.
--force-restart off Override the live-call safeguard for any restart required by this invocation. May interrupt an active call; use only when interruption is explicitly intended.
--gcp-region REGION $CLOUD_ML_REGION or global Vertex AI region
--model MODEL_ID $MODEL_ID or claude-haiku-4-5 Anthropic model for the agent
--adc PATH $GOOGLE_ADC or ~/.config/gcloud/... Path to ADC JSON file
--tenant DOMAIN $TEAMS_TENANT Azure AD tenant domain to verify (e.g. abccorp.com). When specified, the tool checks that Teams is on the correct tenant and automatically switches if needed. Exits with code 6 if the tenant is not in the available list.
--list-tenants off List all available Azure AD tenants and exit. Incompatible with --meeting, --meeting-id, --call-id, and --datetime. See Tenant selection below.
--list-transcripts [DAYS] off List meetings with transcripts for the last N days (default: 1 = today, must be >= 1) and exit. Incompatible with --meeting, --meeting-id, --call-id, and --datetime. Without --tenant, aggregates across all signed-in tenants. See List meetings with transcripts below.
--transcript {yes,no} yes Include transcript cues in output. When no, the transcript download is skipped and output is forced to JSON.
--chat {yes,no} yes Include chat messages in output. When no, chat messages are omitted. If --metadata yes, the tool may still read Event/Call messages internally to build attendee metadata.
--metadata {yes,no} yes Include meeting participant metadata in output. When no, participant data (organiser, invited, speakers, attendees, RSVP) is omitted from the output. Roster extraction still runs if --chat yes (for author name resolution).

Environment variables

All optional arguments can be set via environment variables as fallbacks:

Variable Used by
GOOGLE_CLOUD_PROJECT --gcp-project (required if flag not provided)
CLOUD_ML_REGION --gcp-region
MODEL_ID --model
GOOGLE_ADC --adc
TEAMS_TENANT --tenant

Examples

Download a single-occurrence meeting transcript:

uv run teams-transcripts \
  --meeting 'Project Kickoff' \
  --datetime 2026-04-20 \
  --output ./transcripts/kickoff.vtt \
  --gcp-project my-vertex-project

Download a recurring meeting (the script selects the correct occurrence) based upon the datetime:

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14 \
  --output ./transcripts/standup-2026-04-14.vtt

Disambiguate two meetings with the same name on the same day by adding a time:

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14T14:00 \
  --output ./transcripts/standup-afternoon.vtt

Pipe VTT to stdout (no file written):

uv run teams-transcripts \
  --meeting 'Project Kickoff' \
  --datetime 2026-04-20 | less

Download by meeting ID (from --list-transcripts output):

uv run teams-transcripts \
  --meeting-id 2b4c1f5c-636d-4d78-a087-c2cbf3887fd8 \
  --output ./transcripts/meeting.vtt

This eliminates ambiguity when multiple meetings share the same name. The UUID is shown in the ID column of --list-transcripts table output. The meeting date is resolved automatically from the UUID via the MCPS API.

Download a specific occurrence of a recurring meeting by call ID:

uv run teams-transcripts \
  --call-id aae0280e-5b7e-473d-8a4b-99de3cf74a08 \
  --output ./transcripts/standup-2026-04-29.vtt

The call ID is shown in the Call ID column of --list-transcripts table output. Unlike the meeting UUID (which is shared across all occurrences of a recurring series), the call ID uniquely identifies a specific occurrence. This is the preferred method for recurring meetings.

Get structured JSON output to a file (nothing on stdout):

uv run teams-transcripts \
  --meeting 'Project Kickoff' \
  --datetime 2026-04-20 \
  --output ./kickoff.json --format json

The JSON file includes the full structured transcript as a "transcript" array of cue objects:

{
  "status": "ok",
  "meeting": "Project Kickoff",
  "start": "2026-04-20T10:00+01:00",
  "end": "2026-04-20T11:00+01:00",
  "organiser": {"name": "Alice", "email": "alice@example.com"},
  "invited": [
    {"name": "Alice", "email": "alice@example.com", "rsvp_status": "organizer"},
    {"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"},
    {"name": "Charlie", "email": "charlie@example.com", "rsvp_status": "tentative"}
  ],
  "speakers_list": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"}
  ],
  "attendees": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
    {"name": "Charlie", "email": "charlie@example.com"}
  ],
  "transcript": [
    {
      "id": 1, "start": "00:00:03.000", "end": "00:00:25.000",
      "start_sec": 3.0, "end_sec": 25.0,
      "speaker": "Alice",
      "text": "So I do have this concept of account sets."
    }
  ],
  "chat": [
    {
      "author": "Bob",
      "text": "Here is the link: https://example.com/doc",
      "reactions": [{"type": "like", "count": 2, "users": ["Alice", "Charlie"]}],
      "offset": "00:05:12.000"
    }
  ],
  "parts": 1,
  "cues": 342,
  "speakers": {"Alice": 180, "Bob": 162},
  "file_size": 48210,
  "chat_messages": 2
}

Get structured JSON on stdout (agent-friendly, no file):

uv run teams-transcripts \
  --meeting 'Project Kickoff' \
  --datetime 2026-04-20 --format json

The same schema is written to stdout. With --output, the JSON is written to the file and stdout is empty.

Dry-run to check a meeting exists without downloading:

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14 --dry-run --format json

Quiet mode for cron or agent use (suppress diagnostic logs):

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14 \
  --output ./standup.vtt -q

Use a specific model and region:

uv run teams-transcripts \
  --meeting 'Quarterly Review' \
  --datetime 2026-03-31 \
  --output ./review.vtt \
  --model claude-sonnet-4-20250514 \
  --gcp-region us-east5

Tenant selection

When your Microsoft account has access to multiple Azure AD tenants (organisations), you can list them and target a specific one:

# List all available tenants
uv run teams-transcripts --list-tenants

Text output:

Available tenants (* = current):

    NAME                    DOMAIN                             TENANT ID
  * ABC Corp               abccorp.onmicrosoft.com            86cbe1bb-213f-4271-b174-bd59d03c87a3
    Contoso Ltd             contoso.onmicrosoft.com            2b755fa1-23d1-48f3-98fc-6fdc1dc48d69

Use --tenant NAME_OR_DOMAIN to select a tenant for download.

JSON output:

uv run teams-transcripts --list-tenants --format json
[
  {
    "tenantId": "86cbe1bb-213f-4271-b174-bd59d03c87a3",
    "tenantName": "ABC Corp",
    "domain": "abccorp.onmicrosoft.com",
    "current": true
  },
  {
    "tenantId": "2b755fa1-23d1-48f3-98fc-6fdc1dc48d69",
    "tenantName": "Contoso Ltd",
    "domain": "contoso.onmicrosoft.com",
    "current": false
  }
]

Write the tenant list to a file:

uv run teams-transcripts --list-tenants --output tenants.json --format json

Download a transcript from a specific tenant (auto-switches if needed):

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14 \
  --output ./standup.vtt \
  --tenant contoso.onmicrosoft.com

When --tenant is specified, the tool:

  1. Detects which tenant Teams is currently signed into
  2. Lists all available tenants for the signed-in user
  3. If the current tenant matches, proceeds normally
  4. If the current tenant does not match but the requested tenant is in the list, kills Teams and relaunches it on the correct tenant automatically
  5. If the requested tenant is not in the available list at all, exits with code 6 (TENANT_NOT_FOUND)

The --tenant value is matched case-insensitively against both the tenant name and the primary domain name from the tenant list.

Component selection

By default all three components are extracted: transcript, chat, and metadata. Use the --transcript, --chat, and --metadata flags to select which components to include. At least one must be yes, except with --dry-run, where all three may be no for a pure meeting-exists check.

Extract only chat messages (no transcript download, faster):

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14 \
  --transcript no --metadata no --format json

Extract only participant metadata:

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14 \
  --transcript no --chat no --format json

Extract chat and metadata without the transcript:

uv run teams-transcripts \
  --meeting 'Weekly Standup' \
  --datetime 2026-04-14 \
  --transcript no --format json

When --transcript no is set, the output format is forced to JSON (VTT without transcript cues is meaningless). Explicitly passing --format vtt alongside --transcript no is an error.

When --chat no --metadata yes is set, chat messages are omitted from output, but Event/Call messages may still be read internally to build attendee metadata.

The JSON output is composable: each component adds only its own fields. When all three are yes (the default), the output is identical to the standard full-extraction JSON schema.

MCP server

The package includes a Model Context Protocol (MCP) server that exposes the extraction tools over STDIO. This allows any MCP-compatible client (Claude Desktop, Cursor, OpenCode, etc.) to call the tools directly.

Running the MCP server

From PyPI, with no repository checkout or persistent installation:

# Pinned release (recommended for MCP client configuration)
uvx --from 'teams-transcripts==0.5.2' teams-transcripts-mcp

# Latest release
uvx --from teams-transcripts teams-transcripts-mcp

STDIO is the default transport. When run manually, the command waits quietly for MCP messages on standard input; normally an MCP client starts and manages this process for you.

If the package is already installed, run the entry point directly:

# STDIO transport (default, for subprocess spawning by MCP clients)
teams-transcripts-mcp

# SSE transport on a specific port (for network MCP clients)
teams-transcripts-mcp --port 8080

Or with uv run:

uv run teams-transcripts-mcp
uv run teams-transcripts-mcp --port 8080

Or via the Python module:

uv run python -m teams_transcripts.mcp_server
uv run python -m teams_transcripts.mcp_server --port 8080

When --port is omitted, the server communicates over STDIO (the standard MCP transport for local subprocess spawning). When --port is specified, the server runs as an SSE HTTP server on that port, accessible over the network.

The server inherits environment variables for GCP configuration, including GOOGLE_CLOUD_PROJECT and the optional CLOUD_ML_REGION, MODEL_ID, GOOGLE_ADC, and TEAMS_TENANT settings (see Environment variables).

Available tools

Tool Description
restart_teams Safely relaunch Teams with CDP flags and wait for readiness
list_tenants List available Azure AD tenants
list_transcripts Discover meetings with transcripts (last N days)
download Unified entry point -- accepts UUID or name+date, validates args
download_by_meeting_id Download transcript + chat + metadata by UUID
download_transcript_by_meeting_id Download only transcript by UUID
download_chat_by_meeting_id Download only chat messages by UUID
download_metadata_by_meeting_id Download only participant metadata by UUID
download_all Download transcript + chat + metadata by name + date
download_transcript Download only the transcript (WebVTT cues) by name + date
download_chat Download only chat messages by name + date
download_meeting_metadata Download only participant metadata by name + date

Discovery tools (list_tenants, list_transcripts) accept an optional port parameter and return JSON arrays.

Unified download tool (download) is the recommended entry point for AI agents. It accepts either meeting_id (preferred) or meeting + date, validates argument combinations before launching the subprocess, and supports dry_run=True to verify a meeting can be found without transcript download, output chat extraction, Event/Call attendance extraction, or Details tracking extraction. In dry-run mode, all three component flags may be False for a pure meeting-exists check.

UUID-based download tools (download_by_meeting_id, etc.) accept a meeting_id (UUID from list_transcripts output), optional tenant, port, and dry_run. The meeting subject and date are resolved automatically from the UUID.

Name-based download tools (download_all, etc.) accept meeting (required), date (required), and optional time, tenant, port, and dry_run parameters.

Recommended workflow

1. list_transcripts(days=7)                → discover meetings, get UUIDs
2. download(meeting_id=uuid)               → extract transcript by UUID (preferred)
   OR download(meeting_id=uuid, dry_run=True)  → verify meeting exists first

Or for a known meeting:

1. download(meeting="Weekly Standup", date="2026-04-29")

MCP client configuration with uvx

Use uvx as the MCP command and pass the package plus its MCP executable as arguments. This works with clients that accept the standard JSON MCP server configuration format, including Claude Desktop:

{
  "mcpServers": {
    "teams-transcripts": {
      "command": "uvx",
      "args": [
        "--from",
        "teams-transcripts==0.5.2",
        "teams-transcripts-mcp"
      ],
      "env": {
        "GOOGLE_CLOUD_PROJECT": "your-gcp-project"
      }
    }
  }
}

For Claude Desktop, add that entry to claude_desktop_config.json and restart Claude Desktop. Other MCP clients use the same command and arguments in their local-server configuration. Add any optional environment variables your setup needs to the env object. The MCP process must run as the same macOS user as Teams so it can connect to the desktop app and use that user's credentials.

To track the newest PyPI release instead of pinning one, change the package argument from teams-transcripts==0.5.2 to teams-transcripts.

Configuration from an installed checkout

If the project is already installed in a virtual environment, point the MCP client at its entry point:

{
  "mcpServers": {
    "teams-transcripts": {
      "command": "/path/to/teams-transcripts/bin/teams-transcripts-mcp",
      "env": {
        "GOOGLE_CLOUD_PROJECT": "your-gcp-project"
      }
    }
  }
}

For a source checkout managed by uv, configure the client to run in the repository directory:

{
  "mcpServers": {
    "teams-transcripts": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/teams-transcripts", "teams-transcripts-mcp"],
      "env": {
        "GOOGLE_CLOUD_PROJECT": "your-gcp-project"
      }
    }
  }
}

Architecture

Each MCP tool shells out to the teams-transcripts CLI with --format json --quiet and returns the parsed JSON response. The subprocess approach provides:

  • Process isolation (Teams CDP locks are per-process)
  • Clean signal handling (the child manages its own SIGINT/SIGTERM)
  • No import-time side effects in the MCP server process

Discovery tools (list_tenants, list_transcripts) return JSON arrays. Download tools return a JSON object with the extraction results. UUID-based tools use --meeting-id internally; name-based tools use --meeting + --datetime.

Safe Teams restarts

Every automatic Teams restart checks for a possible live call immediately before killing the app. The check samples CoreAudio activity for the Teams app and its helper processes three times, then checks the macOS Accessibility tree for a visible Leave or Hang-up control. The result is active, inactive, or unknown; both active and unknown block automatic CDP recovery and tenant switching.

Blocked CLI flows exit with code 2 and the machine-stable RESTART_BLOCKED error. MCP callers receive the same error through download/listing tools. The restart_teams MCP tool instead returns a structured result containing status, restarted, cdp_ready, port, call_state, and forced. A block also includes reason: active_call or reason: call_state_unknown.

The CLI flag --force-restart overrides the safeguard for any automatic CDP recovery or tenant switch required by that invocation. The MCP equivalent is restart_teams(force=True). Both may interrupt a live call and must be used only when interruption is explicitly intended. Without an override, downloads, listing, tenant switching, and recovery continue to fail closed.

Output format

VTT file structure

The output is a standard WebVTT file containing three sections:

1. Meeting metadata -- a NOTE Meeting Metadata comment block at the top:

WEBVTT

NOTE Meeting Metadata
Meeting: Weekly Standup
Organiser: Alice <alice@example.com>
Invited: Alice, Bob, Charlie
Speakers: Alice, Bob
Attendees: Alice, Bob, Charlie
Start: 2026-04-14T10:00+01:00
End: 2026-04-14T10:30+01:00

For larger meetings (more than 10 invited or attendees), counts are shown instead of names, with an RSVP breakdown when tracking data is available:

NOTE Meeting Metadata
Meeting: Q1 2026 All Hands
Organiser: Suman Landa <suman.landa@example.com>
Invited: 90 members (50 accepted, 5 tentative, 2 declined, 33 no response)
Speakers: Alice, Bob, Charlie, Dave, Eve, Fran
Attendees: 77 participants
Start: 2026-04-01T09:00+01:00
End: 2026-04-01T10:30+01:00

Fields:

  • Meeting: meeting title.
  • Organiser: meeting creator with email in Name <email> format.
  • Invited: all members on the meeting chat thread. Listed by name when 10 or fewer; shown as a count with RSVP summary when more than 10. RSVP statuses are: accepted, tentative, declined, no response.
  • Speakers: members who spoke during the meeting (from Recap).
  • Attendees: members who joined the call (from Event/Call partlist). May be incomplete for large meetings due to server-side truncation.
  • Start / End: ISO 8601 timestamps with timezone offset.

2. Chat messages -- a NOTE Chat comment block containing a JSON array of messages sent during the meeting:

NOTE Chat
[
  {
    "author": "Bob",
    "text": "Link to the design doc: https://example.com/doc",
    "reactions": [
      {"type": "like", "count": 2, "users": ["Alice", "Charlie"]}
    ],
    "offset": "00:12:34.567"
  }
]

Each message includes: author name, plain text (HTML stripped, links preserved), reactions (with per-user attribution where resolvable), and an offset from meeting start in HH:MM:SS.mmm format where computable. The offset field is null when it cannot be computed. The reactions field is null when there are no reactions.

3. Transcript cues -- standard WebVTT cues with speaker identification:

1
00:00:03.000 --> 00:00:25.000
<v Alice>So I do have this concept of account sets. It's called
platform account sets in the code base, which basically...</v>

2
00:00:27.000 --> 00:00:48.000
<v Bob>Looks something like this. For example, this is our dev
accounts.</v>

Each cue contains a sequential number, start and end timestamps in HH:MM:SS.mmm format, the speaker name in a <v Name> voice span, and the spoken text.

For multi-part meetings (where Teams splits the recording), cue timestamps are offset so that Part 2 continues from where Part 1 ended, producing a single continuous timeline.

JSON output schemas

When --format json is used, a single JSON object is written to stdout (when --output is omitted) or to the specified file (when --output is given). In file mode, nothing is written to stdout.

Success (all components):

{
  "status": "ok",
  "meeting": "Weekly Standup",
  "start": "2026-04-14T10:00+01:00",
  "end": "2026-04-14T10:30+01:00",
  "organiser": {"name": "Alice", "email": "alice@example.com"},
  "invited": [
    {"name": "Alice", "email": "alice@example.com", "rsvp_status": "organizer"},
    {"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"},
    {"name": "Charlie", "email": "charlie@example.com", "rsvp_status": "declined"}
  ],
  "speakers_list": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"}
  ],
  "attendees": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
    {"name": "Charlie", "email": "charlie@example.com"}
  ],
  "chat": [
    {
      "author": "Bob",
      "text": "Here is the link: https://example.com/doc",
      "reactions": [{"type": "like", "count": 2, "users": ["Alice", "Charlie"]}],
      "offset": "00:05:12.000"
    },
    {
      "author": "Alice",
      "text": "Thanks Bob",
      "reactions": null,
      "offset": "00:05:45.000"
    }
  ],
  "transcript": [
    {
      "id": 1, "start": "00:00:03.000", "end": "00:00:25.000",
      "start_sec": 3.0, "end_sec": 25.0,
      "speaker": "Alice",
      "text": "So I do have this concept of account sets."
    },
    {
      "id": 2, "start": "00:00:27.000", "end": "00:00:48.000",
      "start_sec": 27.0, "end_sec": 48.0,
      "speaker": "Bob",
      "text": "Looks something like this."
    }
  ],
  "parts": 1,
  "cues": 342,
  "speakers": {"Alice": 180, "Bob": 162},
  "file_size": 48210,
  "chat_messages": 2
}

Dry-run (--dry-run --format json):

{
  "status": "ok",
  "dry_run": true,
  "meeting": "Weekly Standup",
  "start": "2026-04-14T10:00+01:00",
  "end": "2026-04-14T10:30+01:00",
  "organiser": {"name": "Alice", "email": "alice@example.com"},
  "invited": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
    {"name": "Charlie", "email": "charlie@example.com"}
  ],
  "speakers_list": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"}
  ],
  "parts": 1,
  "speakers": {"Alice": 0, "Bob": 0}
}

Speaker counts are zero in dry-run mode (no transcript downloaded). The "cues", "file_size", "transcript", and "chat_messages" keys are absent. "attendees" is absent in dry-run because Event/Call partlists are only available after chat extraction, which dry-run skips.

Error (any non-zero exit):

{
  "status": "error",
  "code": "MEETING_NOT_FOUND",
  "message": "Could not find the target 2026-04-14 in any of the first 4 search results.",
  "step": "step5"
}

The "code" field is a machine-stable error code (see --help for the full list). The "step" field indicates which pipeline step failed (absent for argument validation errors).

Conditional keys: "end" is absent if the meeting time text could not be parsed. "organiser" is absent if the organiser could not be identified. "invited" is absent if roster extraction failed entirely (popover unavailable). "speakers_list" is absent if no speakers were found. "attendees" is absent if no Event/Call partlist was available (e.g. dry-run mode, or the meeting had no call join/leave events). "chat_messages" is absent (not zero) when there are no chat messages. "rsvp_status" is included on each invited member only when RSVP tracking data was available; it is omitted (not empty) when the Details tab Tracking panel could not be read.

Component-specific JSON schemas

The JSON output is composable. Each component (--transcript, --chat, --metadata) contributes only its own fields. Base fields (status, meeting, start, end) are always present regardless of which components are selected.

Transcript only (--transcript yes --chat no --metadata no):

{
  "status": "ok",
  "meeting": "Weekly Standup",
  "start": "2026-04-14T10:00+01:00",
  "end": "2026-04-14T10:30+01:00",
  "parts": 1,
  "cues": 342,
  "speakers": {"Alice": 180, "Bob": 162},
  "file_size": 48210,
  "transcript": [
    {
      "id": 1, "start": "00:00:03.000", "end": "00:00:25.000",
      "start_sec": 3.0, "end_sec": 25.0,
      "speaker": "Alice",
      "text": "So I do have this concept of account sets."
    }
  ]
}

Transcript-specific fields: parts, cues, speakers (name-to-cue-count map), file_size, and transcript (array of cue objects).

Chat only (--transcript no --chat yes --metadata no):

{
  "status": "ok",
  "meeting": "Weekly Standup",
  "start": "2026-04-14T10:00+01:00",
  "end": "2026-04-14T10:30+01:00",
  "chat": [
    {
      "author": "Bob",
      "text": "Here is the link: https://example.com/doc",
      "reactions": [{"type": "like", "count": 2, "users": ["Alice", "Charlie"]}],
      "offset": "00:05:12.000"
    },
    {
      "author": "Alice",
      "text": "Thanks Bob",
      "reactions": null,
      "offset": "00:05:45.000"
    }
  ]
}

Chat-specific fields: chat (array of message objects). Each message has:

  • author (string): display name of the sender
  • text (string): plain text content (HTML stripped, links preserved)
  • reactions (array or null): list of reaction objects, each with type, count, and users (array of display names)
  • offset (string or null): time offset from meeting start in HH:MM:SS.mmm format when computable

Metadata only (--transcript no --chat no --metadata yes):

{
  "status": "ok",
  "meeting": "Weekly Standup",
  "start": "2026-04-14T10:00+01:00",
  "end": "2026-04-14T10:30+01:00",
  "organiser": {"name": "Alice", "email": "alice@example.com"},
  "invited": [
    {"name": "Alice", "email": "alice@example.com", "rsvp_status": "organizer"},
    {"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"},
    {"name": "Charlie", "email": "charlie@example.com", "rsvp_status": "tentative"}
  ],
  "speakers_list": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"}
  ],
  "attendees": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
    {"name": "Charlie", "email": "charlie@example.com"}
  ]
}

Metadata-specific fields:

  • organiser (object): {name, email} of the meeting creator
  • invited (array): members on the chat thread, each with name, email, and optionally rsvp_status (one of: organizer, accepted, tentative, declined, none)
  • speakers_list (array): members who spoke, each with name and email
  • attendees (array): members who joined the call, each with name and email

Chat + metadata (--transcript no --chat yes --metadata yes):

{
  "status": "ok",
  "meeting": "Weekly Standup",
  "start": "2026-04-14T10:00+01:00",
  "end": "2026-04-14T10:30+01:00",
  "organiser": {"name": "Alice", "email": "alice@example.com"},
  "invited": [
    {"name": "Alice", "email": "alice@example.com", "rsvp_status": "accepted"},
    {"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"}
  ],
  "speakers_list": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"}
  ],
  "attendees": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"}
  ],
  "chat": [
    {
      "author": "Bob",
      "text": "Starting now",
      "reactions": null,
      "offset": "00:00:05.000"
    }
  ]
}

List tenants (--list-tenants --format json):

[
  {
    "tenantId": "86cbe1bb-213f-4271-b174-bd59d03c87a3",
    "tenantName": "ABC Corp",
    "domain": "abccorp.onmicrosoft.com",
    "current": true
  },
  {
    "tenantId": "2b755fa1-23d1-48f3-98fc-6fdc1dc48d69",
    "tenantName": "Contoso Ltd",
    "domain": "contoso.onmicrosoft.com",
    "current": false
  }
]

JSON field reference

Field Type Present when Description
status string always "ok" on success, "error" on failure
meeting string always (success) Meeting title
start string always (success) ISO 8601 meeting start time
end string when parseable ISO 8601 meeting end time
dry_run boolean --dry-run mode Always true when present
organiser object --metadata yes {name, email}
invited array --metadata yes [{name, email, rsvp_status?}]
speakers_list array --metadata yes [{name, email}]
attendees array --metadata yes + attendance data available [{name, email}]
chat array --chat yes + messages exist [{author, text, reactions, offset}]
chat_messages integer all-components mode Count of chat messages
parts integer --transcript yes Number of transcript parts
cues integer --transcript yes Total cue count
speakers object --transcript yes {name: cue_count} map
file_size integer --transcript yes VTT content size in bytes
transcript array --transcript yes [{id, start, end, start_sec, end_sec, speaker, text}]
code string error Machine-stable error code
message string error Human-readable error description
step string error (runtime) Pipeline step that failed

Recurring meetings

For recurring meetings, the script:

  1. Navigates to the meeting chat group
  2. Opens the Recap tab and reads the occurrence dropdown
  3. If the default occurrence does not match the target date, opens the dropdown and scrolls through available dates
  4. Uses the LLM agent to identify and select the correct occurrence

Teams typically retains the most recent 5 or so occurrences in the dropdown. Older occurrences may no longer be available.

If the first search result leads to a chat group that does not contain the target date, the script automatically retries with the next search result (up to 4 attempts).

Search modes

Teams uses two different search UI patterns depending on the context:

  • Panel mode: results appear in a side panel with text-based matching. The script scores results by word overlap with the meeting name.
  • Popup mode: results appear in a dropdown overlay. The script uses the LLM agent to select the correct result from a screenshot.

The mode is auto-detected; no configuration is needed.

Limitations

  • macOS only: the script uses macOS-specific commands (open -a, pkill) to manage the Teams process.
  • Occurrence dropdown depth: Teams only retains recent occurrences (roughly the last 5). Older recurring meeting dates cannot be retrieved.
  • Attendee list truncation: the Event/Call <partlist> XML that Teams stores in chat messages is truncated server-side for large meetings. For example, a meeting declaring count="77" may only include 25 <part> elements. The attendee list in the output may therefore be incomplete. This is a Teams limitation with no client-side workaround.
  • RSVP data is calendar responses, not actual attendance: the tracking panel shows who accepted/declined/tentatively accepted the calendar invitation. It does not reflect who actually joined the call. Actual attendance is determined by the <partlist> (see above).
  • RSVP extraction is soft-failure: if the Details tab or Tracking panel is unavailable (e.g. the meeting type does not support tracking, or the UI layout changes), the pipeline continues without RSVP data. The rsvp_status field will be absent from JSON output in this case.
  • Conference room accounts: room/resource accounts appear with their system names (e.g. UK-LON-1SB-05S19@oursystems.com) as that is what Teams stores as the display name.

Exit codes

Code Name Meaning
0 EXIT_OK Success
1 EXIT_BAD_ARGS Invalid arguments or missing configuration
2 EXIT_CDP_UNAVAILABLE Teams/CDP not running or not responding
3 EXIT_MEETING_NOT_FOUND Meeting not found after all retries
4 EXIT_DOWNLOAD_FAILED Transcript download or permission error
5 EXIT_VERIFICATION_FAILED Output file is invalid
6 EXIT_TENANT_NOT_FOUND Requested tenant not in available tenants list
130 EXIT_INTERRUPTED Interrupted by SIGINT (Ctrl+C)
143 EXIT_TERMINATED Terminated by SIGTERM

Error codes

Each error includes a machine-stable code string used as a [PREFIX] on stderr and as the "code" field in JSON error output. These are stable identifiers suitable for programmatic matching.

Code Meaning
BAD_ARGS Invalid CLI arguments or missing configuration
CDP_UNAVAILABLE Teams/CDP not running or not responding
RESTART_BLOCKED Teams restart blocked by an active or unknown call state
NO_TARGETS No Teams page targets found via CDP
SEARCH_FAILED Search results did not appear in the UI
MEETING_NOT_FOUND Meeting not found in search results
RECAP_NOT_FOUND Recap tab not found after navigation
TRANSCRIPT_NOT_FOUND Transcript tab not found
IFRAME_NOT_FOUND No transcript iframe target among CDP targets
DOWNLOAD_FAILED VTT download failed
VERIFICATION_FAILED Output file content is invalid
TENANT_NOT_FOUND Requested tenant not in available tenants list
INTERRUPTED Process interrupted by SIGINT
TERMINATED Process terminated by SIGTERM

Agent and automation usage

The CLI is designed for use by both humans and automated agents. Key features for machine consumption:

  • stdout/stderr separation: all diagnostic output goes to stderr. stdout carries only data -- VTT content in text mode, or a JSON object in JSON mode. When --output is given, nothing is written to stdout (both formats). Use --quiet to suppress diagnostics entirely.
  • --format json: emits a single JSON object. On success, the object contains meeting metadata, speaker counts, cue count, and a "transcript" array of structured cue objects. On failure, the object contains "status": "error", a machine-stable "code" string, and a human message.
  • Semantic exit codes: agents can branch on the exit code without parsing error messages. See the table above.
  • Machine-stable error codes: each error includes a short code prefix in text mode (e.g. [MEETING_NOT_FOUND]) and a "code" field in JSON mode. Codes are listed in --help under "error codes".
  • --dry-run: finds the meeting, loads Recap only when needed, extracts safe roster metadata when requested, and detects parts without downloading the transcript, extracting chat/attendance data, or opening Details tracking. Useful for validation before committing to a full download.
  • Signal handling: SIGINT and SIGTERM are handled cleanly. The first signal sets a shutdown flag and the process exits at the next safe point (within 500 ms). A second signal force-exits immediately. JSON error output is emitted on signal exit when --format json is active.

Output modes

--output --format --dry-run stdout file
PATH vtt no (empty) VTT
PATH json no (empty) JSON
omitted vtt no raw VTT content (none)
omitted json no structured JSON (none)
omitted vtt yes (empty) (none)
omitted json yes JSON metadata (no transcript) (none)
PATH any yes error (incompatible) --
--list-tenants vtt -- tenant table (none)
--list-tenants json -- tenant JSON array (none)
--list-tenants + PATH vtt -- (empty) tenant table
--list-tenants + PATH json -- (empty) tenant JSON
--list-transcripts table -- meeting table (none)
--list-transcripts json -- meeting JSON array (none)
--list-transcripts vtt -- error (incompatible) --
--list-transcripts + PATH table -- (empty) meeting table
--list-transcripts + PATH json -- (empty) meeting JSON

Development

Setup

git clone <repo-url> && cd teams-transcripts
uv sync --all-groups --locked
lefthook install

uv sync creates the virtual environment in .venv/ and installs all runtime and development dependencies from the lockfile. Lefthook is an external development prerequisite; install it with brew install lefthook on macOS before running lefthook install to set up the pre-commit hooks.

To regenerate the live regression fixtures:

uv run pytest tests/regression/test_regression.py -m live --update-fixtures

See Generating regression tests for details.

After setup, the wrapper scripts in bin/ are ready to use:

./bin/teams-transcripts --help
./bin/teams-transcripts-mcp --help

These delegate to the venv entry points without requiring uv run.

Pre-commit hooks

This project uses Lefthook for pre-commit checks. The following checks run in parallel on every commit:

Check Tool Description
ruff-check Ruff Lint with an extended rule set (Pyflakes, pycodestyle, Bugbear, Bandit, and more)
ruff-format Ruff Verify formatting matches the canonical style
ty-check ty Type checking with --error-on-warning

All three checks must pass before a commit is accepted. Configuration is in pyproject.toml.

Running checks manually

uv run ruff check teams_transcripts/ legacy/ tests/ scripts/
uv run ruff format --check teams_transcripts/ legacy/ tests/ scripts/
uv run ty check --error-on-warning teams_transcripts/ legacy/ tests/ scripts/

These checks exclude tests/regression/, which is a local, generated test tree containing private meeting configuration and fixtures.

Adding a dependency

uv add <package>

This updates both pyproject.toml and uv.lock.

Publishing a release

Version tags publish the verified wheel and source distribution to PyPI through GitLab CI/CD. The publish job runs only for tags such as v0.6.0, only after both verification jobs and the package job succeed, and uses PyPI Trusted Publishing with a short-lived GitLab OIDC token. No PyPI password or API token is stored in GitLab.

Before the first automated release, add a GitLab CI/CD trusted publisher under the PyPI project's Manage > Publishing settings with these values:

Setting Value
Namespace jamiemills-choreograph
Project teams-transcripts
Top-level pipeline file .gitlab-ci.yml
Environment pypi

To release a new version:

uv version 0.6.0 --no-sync
git add pyproject.toml uv.lock
git commit -m "Prepare 0.6.0 release"
git push origin main

# After the main pipeline succeeds:
git tag -a v0.6.0 -m "teams-transcripts 0.6.0"
git push origin v0.6.0

The package job verifies that the tag exactly matches the built package version before publishing. PyPI releases are immutable, so never move or reuse a version tag; increment the project version and create a new tag instead.

Running tests

# Safe local suite (live, destructive, and fixture-data tests are deselected)
uv run pytest

# Tracked tests only (for fresh clones without regression/ directory)
uv run pytest tests/ --ignore=tests/regression

# Verbose output with test names
uv run pytest -v

# Specific test file
uv run pytest tests/test_domain_models.py

# Live regression tests (requires Teams running with CDP on port 9333)
uv run pytest -m live

# Override the safe default and run every collected test (may relaunch Teams)
uv run pytest -m ""

The test suite uses four custom pytest markers. The default marker expression excludes live, destructive, and fixtures; passing -m explicitly overrides that default.

Marker Requires Description
live Running Teams + CDP Regression tests against real meetings
offline Generated local configuration Offline integration accuracy tests
destructive Running Teams Tests that kill and relaunch the Teams process
fixtures Generated fixture files Offline tests that validate fixture VTT content

Generating regression tests

The tests/regression/fixtures/ directory and local meeting config may contain real meeting data. Treat regenerated fixtures as sensitive and do not publish them outside the repository context.

The regression test suite compares tool output byte-for-byte against reference VTT files in tests/regression/fixtures/. These must be generated from a live Teams session:

uv run pytest tests/regression/test_regression.py -m live --update-fixtures

Requirements:

  • Microsoft Teams running with CDP enabled on port 9333
  • Valid GCP credentials for Vertex AI (the LLM agent)
  • The 9 meetings defined in tests/regression/test_meetings.yaml accessible in Teams

This downloads each meeting's transcript and saves it as tests/regression/fixtures/regression-{A-I}.vtt. Once generated, the fixture validation tests run without Teams access.

To verify fixture-dependent tests pass after generation:

uv run pytest tests/regression/ -m "not live and not destructive"

The regression suite validates the 9 reference meetings in three modes: download (byte-for-byte comparison against fixtures), dry-run (JSON metadata schema), and format-json (output schema validation).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

teams_transcripts-0.5.2.tar.gz (364.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

teams_transcripts-0.5.2-py3-none-any.whl (186.0 kB view details)

Uploaded Python 3

File details

Details for the file teams_transcripts-0.5.2.tar.gz.

File metadata

  • Download URL: teams_transcripts-0.5.2.tar.gz
  • Upload date:
  • Size: 364.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for teams_transcripts-0.5.2.tar.gz
Algorithm Hash digest
SHA256 aece118f5754623d60fd9a145399d5abeedab813ec2f7bb13827810159943f4d
MD5 5a40e13b7f07b9f30d4c157f64191eba
BLAKE2b-256 53a029df00b25f382af7d6db0ac333aa8bd06c9d9e65e0bdc74a598b9f42c649

See more details on using hashes here.

File details

Details for the file teams_transcripts-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: teams_transcripts-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 186.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for teams_transcripts-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 c30d820e819ac6d08d309c7e28f60abe69f74ac6e3615085bfb9e06669ab1103
MD5 ae9f74054e5d4429f90d3ebefd072ff3
BLAKE2b-256 4b3f2051499a8cc2387be38ed83914f141387dadce1d4548313c2a569043d80a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page