teams-transcripts 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- legacy/__init__.py +0 -0
- legacy/transcript_download.py +3969 -0
- teams_transcripts/__init__.py +1 -0
- teams_transcripts/__main__.py +288 -0
- teams_transcripts/adapters/__init__.py +1 -0
- teams_transcripts/adapters/cdp/__init__.py +1 -0
- teams_transcripts/adapters/cdp/browser.py +196 -0
- teams_transcripts/adapters/cdp/connection.py +168 -0
- teams_transcripts/adapters/cdp/helpers.py +449 -0
- teams_transcripts/adapters/cdp/teams.py +463 -0
- teams_transcripts/adapters/vertex.py +274 -0
- teams_transcripts/domain/__init__.py +1 -0
- teams_transcripts/domain/chat.py +115 -0
- teams_transcripts/domain/html.py +126 -0
- teams_transcripts/domain/mcps.py +343 -0
- teams_transcripts/domain/models.py +531 -0
- teams_transcripts/domain/participants.py +292 -0
- teams_transcripts/domain/scoring.py +106 -0
- teams_transcripts/domain/speakers.py +130 -0
- teams_transcripts/domain/summary.py +266 -0
- teams_transcripts/domain/timestamps.py +350 -0
- teams_transcripts/domain/vtt.py +444 -0
- teams_transcripts/infrastructure/__init__.py +1 -0
- teams_transcripts/infrastructure/config.py +987 -0
- teams_transcripts/infrastructure/errors.py +120 -0
- teams_transcripts/infrastructure/logging.py +69 -0
- teams_transcripts/infrastructure/signals.py +143 -0
- teams_transcripts/mcp_server.py +818 -0
- teams_transcripts/ports/__init__.py +1 -0
- teams_transcripts/ports/agent.py +58 -0
- teams_transcripts/ports/browser.py +126 -0
- teams_transcripts/services/__init__.py +1 -0
- teams_transcripts/services/downloader.py +1283 -0
- teams_transcripts/services/extraction.py +1603 -0
- teams_transcripts/services/listing.py +826 -0
- teams_transcripts/services/navigation.py +1721 -0
- teams_transcripts/services/output.py +84 -0
- teams_transcripts/services/tenant.py +684 -0
- teams_transcripts-0.5.0.dist-info/METADATA +1438 -0
- teams_transcripts-0.5.0.dist-info/RECORD +42 -0
- teams_transcripts-0.5.0.dist-info/WHEEL +4 -0
- teams_transcripts-0.5.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,1438 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: teams-transcripts
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Download Microsoft Teams transcripts, chat, and meeting metadata from the macOS desktop app
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/jamiemills-choreograph/teams-transcripts
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/jamiemills-choreograph/teams-transcripts
|
|
7
|
+
Project-URL: Issues, https://gitlab.com/jamiemills-choreograph/teams-transcripts/-/issues
|
|
8
|
+
Keywords: macos,mcp,microsoft-teams,transcripts,webvtt
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: MacOS X
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Communications :: Conferencing
|
|
18
|
+
Classifier: Topic :: Text Processing
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: anthropic[vertex]
|
|
21
|
+
Requires-Dist: mcp>=1.27.0
|
|
22
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Requires-Dist: websockets
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
27
|
+
Requires-Dist: pyyaml>=6.0; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Teams Transcript Downloader
|
|
31
|
+
|
|
32
|
+
A command-line tool that downloads Microsoft Teams meeting transcripts, chat
|
|
33
|
+
messages, and participant metadata as WebVTT (`.vtt`) or structured JSON. It
|
|
34
|
+
connects to the Teams desktop app via the Chrome DevTools Protocol (CDP) and
|
|
35
|
+
navigates the UI programmatically. An LLM agent (Claude on Vertex AI) handles
|
|
36
|
+
bounded non-deterministic steps such as selecting ambiguous search results,
|
|
37
|
+
selecting recurring-meeting occurrences, and disambiguating iframe targets.
|
|
38
|
+
|
|
39
|
+
## Run with uvx
|
|
40
|
+
|
|
41
|
+
On macOS with [uv](https://docs.astral.sh/uv/) installed, run the app directly
|
|
42
|
+
from PyPI without a persistent installation:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uvx teams-transcripts --help
|
|
46
|
+
uvx teams-transcripts \
|
|
47
|
+
--meeting "Weekly Standup" \
|
|
48
|
+
--datetime 2026-07-11 \
|
|
49
|
+
--gcp-project your-project
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
To run one of the package's differently named commands, specify the package:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uvx --from teams-transcripts teams-transcripts-mcp
|
|
56
|
+
uvx --from teams-transcripts transcript-download --help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`uvx` creates an isolated temporary environment. The Teams desktop app, CDP,
|
|
60
|
+
and authentication prerequisites below still apply.
|
|
61
|
+
|
|
62
|
+
## How it works
|
|
63
|
+
|
|
64
|
+
The script executes an 8-step pipeline:
|
|
65
|
+
|
|
66
|
+
| Step | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| 1 | Verify Teams is running with CDP enabled; relaunch if needed |
|
|
69
|
+
| 2 | Confirm CDP is responding on the configured port |
|
|
70
|
+
| 3 | Discover Teams page targets via CDP |
|
|
71
|
+
| 3b | _(when `--tenant` specified)_ Verify tenant; switch if needed |
|
|
72
|
+
| 3c | _(when `--meeting-id` used)_ Resolve UUID to meeting subject and date via MCPS API |
|
|
73
|
+
| 4 | Type the meeting name into the Teams search bar and navigate to the chat |
|
|
74
|
+
| 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 |
|
|
75
|
+
| 6 | Locate the transcript iframe target when transcript output is requested |
|
|
76
|
+
| 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 |
|
|
77
|
+
| 8 | Validate generated transcript content when transcript output is requested |
|
|
78
|
+
|
|
79
|
+
### Download approaches
|
|
80
|
+
|
|
81
|
+
When transcript output is requested, the script detects which approach to use
|
|
82
|
+
based on the Download button state:
|
|
83
|
+
|
|
84
|
+
**Approach A -- Native blob interception** is used when you have download
|
|
85
|
+
permission. The script clicks the Download button and intercepts the blob URL
|
|
86
|
+
to capture the VTT content byte-for-byte.
|
|
87
|
+
|
|
88
|
+
**Approach B -- React fibre tree extraction** is used when download is blocked
|
|
89
|
+
(the button shows "You don't have permission"). The script walks the React
|
|
90
|
+
component tree to extract all transcript entries directly from the in-memory
|
|
91
|
+
data structure, then generates a well-formed VTT file.
|
|
92
|
+
|
|
93
|
+
### Speaker name resolution
|
|
94
|
+
|
|
95
|
+
Speaker names are resolved using the Recap Speakers tab. The script extracts
|
|
96
|
+
a GUID-to-display-name mapping and applies it to clean up:
|
|
97
|
+
|
|
98
|
+
- **Vendor-prefixed names**: `v-John Doe (Metosin)` becomes `John Doe`
|
|
99
|
+
- **Suffixed names**: `Jane Smith GRM` becomes `Jane Smith`
|
|
100
|
+
- **Synthetic placeholders**: `@1` becomes `Speaker 1`
|
|
101
|
+
|
|
102
|
+
Names that are already clean (e.g. internal staff) are left unchanged.
|
|
103
|
+
|
|
104
|
+
### Meeting participant data
|
|
105
|
+
|
|
106
|
+
The tool extracts categorised participant information from four sources:
|
|
107
|
+
|
|
108
|
+
| Category | Source | Description |
|
|
109
|
+
|----------|--------|-------------|
|
|
110
|
+
| Organiser | Roster popover (fibre tree) | The meeting creator, identified by `organizerId` from the chat topic menu |
|
|
111
|
+
| Invited | Roster popover (fibre tree) | All members of the meeting chat thread, with names, emails, and Azure AD object IDs |
|
|
112
|
+
| Speakers | Recap Speakers tab | Members who spoke during the meeting, cross-referenced with the roster by display name |
|
|
113
|
+
| Attendees | Event/Call `<partlist>` XML in chat messages | Members who joined the call, cross-referenced with the roster by Azure AD object ID |
|
|
114
|
+
|
|
115
|
+
For the **invited** category, RSVP tracking data can optionally be enriched
|
|
116
|
+
from the meeting Details tab. The tool navigates to Details, opens the
|
|
117
|
+
Tracking panel, clicks "Show more attendees" if present, and parses the
|
|
118
|
+
aria-labels to extract each invitee's calendar response status (accepted,
|
|
119
|
+
tentative, declined, or no response). This enrichment is soft-failure:
|
|
120
|
+
if the Tracking panel is unavailable, the pipeline continues without RSVP
|
|
121
|
+
data.
|
|
122
|
+
|
|
123
|
+
The roster popover is accessed by clicking the participant count button in
|
|
124
|
+
the chat header. This retrieves the full member list (names, emails,
|
|
125
|
+
object IDs) for all members, regardless of meeting size. The legacy
|
|
126
|
+
`membersLimited` property in the fibre tree is intentionally limited to
|
|
127
|
+
~3 members by Teams and is only used as a fallback.
|
|
128
|
+
|
|
129
|
+
## Prerequisites
|
|
130
|
+
|
|
131
|
+
### 1. Microsoft Teams desktop app (macOS)
|
|
132
|
+
|
|
133
|
+
Teams must be running with CDP remote debugging enabled. If Teams is already
|
|
134
|
+
running without CDP, the script will kill and relaunch it automatically with
|
|
135
|
+
the full set of required flags.
|
|
136
|
+
|
|
137
|
+
To launch manually, only the CDP port is strictly required:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
export WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS='--remote-debugging-port=9333'
|
|
141
|
+
open -a 'Microsoft Teams'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When the script relaunches Teams itself, it sets additional Chromium flags for
|
|
145
|
+
reliable automation:
|
|
146
|
+
|
|
147
|
+
| Flag | Purpose |
|
|
148
|
+
|------|---------|
|
|
149
|
+
| `--disable-backgrounding-occluded-windows` | Keep rendering when Teams is behind other windows |
|
|
150
|
+
| `--disable-renderer-backgrounding` | Keep renderer at full priority when unfocused |
|
|
151
|
+
| `--disable-hang-monitor` | Suppress "page unresponsive" dialogs |
|
|
152
|
+
| `--disable-ipc-flooding-protection` | Prevent throttling of rapid CDP commands |
|
|
153
|
+
| `--disable-smooth-scrolling` | Remove animation delays |
|
|
154
|
+
| `--run-all-compositor-stages-before-draw` | Ensure fully composited frames for screenshots |
|
|
155
|
+
| `--hide-scrollbars` | Cleaner screenshots |
|
|
156
|
+
| `--mute-audio` | Suppress notification and meeting sounds |
|
|
157
|
+
| `--silent-debugger-extension-api` | Hide "debugging" infobars |
|
|
158
|
+
| `--force-device-scale-factor=2` | Retina-quality screenshots for the LLM agent |
|
|
159
|
+
|
|
160
|
+
### 2. Google Cloud credentials
|
|
161
|
+
|
|
162
|
+
The LLM agent uses Claude via Vertex AI, so you need Application Default
|
|
163
|
+
Credentials (ADC) for a GCP project with the Vertex AI API enabled:
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
gcloud auth application-default login
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Or point the `--adc` flag (or `GOOGLE_ADC` env var) at a service account key
|
|
170
|
+
file.
|
|
171
|
+
|
|
172
|
+
### 3. Python dependencies
|
|
173
|
+
|
|
174
|
+
The script requires Python 3.10+ and is managed with
|
|
175
|
+
[uv](https://docs.astral.sh/uv/). To install all dependencies:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
uv sync --locked
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This creates a virtual environment in `.venv/` and installs the locked
|
|
182
|
+
dependencies from `uv.lock`.
|
|
183
|
+
|
|
184
|
+
## Usage
|
|
185
|
+
|
|
186
|
+
### Download a transcript
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
teams-transcripts (--meeting NAME | --meeting-id UUID) [--datetime YYYY-MM-DD[THH:MM]] [--output PATH] [options]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Or with `uv run` (no wrapper script needed):
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
uv run teams-transcripts (--meeting NAME | --meeting-id UUID) [--datetime YYYY-MM-DD[THH:MM]] [--output PATH] [options]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Or via the Python module:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
uv run python -m teams_transcripts (--meeting NAME | --meeting-id UUID) [--datetime YYYY-MM-DD[THH:MM]] [--output PATH] [options]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### List available tenants
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
teams-transcripts --list-tenants [--format json] [--output PATH]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Lists all Azure AD tenants the signed-in user can access, marks the current
|
|
211
|
+
tenant with `*`, then exits. Use the name or domain value from the output
|
|
212
|
+
with `--tenant` in a subsequent download invocation.
|
|
213
|
+
|
|
214
|
+
### List meetings with transcripts
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
teams-transcripts --list-transcripts [DAYS] [--format json] [--output PATH] [--tenant DOMAIN]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Lists all meetings with transcripts that the signed-in user attended. `DAYS`
|
|
221
|
+
must be `1` or greater. This
|
|
222
|
+
queries the Teams MCPS (Meeting Content and Presence Service) API via CDP
|
|
223
|
+
worker injection -- no Graph API permissions or app registrations required.
|
|
224
|
+
|
|
225
|
+
The optional `DAYS` argument controls how many days back to search (default:
|
|
226
|
+
1 = today only).
|
|
227
|
+
|
|
228
|
+
**Multi-tenant behaviour:** when `--tenant` is specified, only the matching
|
|
229
|
+
tenant is queried. Without `--tenant`, all signed-in tenants are queried
|
|
230
|
+
and results aggregated into a single output.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
# Today's meetings with transcripts (all tenants)
|
|
236
|
+
teams-transcripts --list-transcripts
|
|
237
|
+
|
|
238
|
+
# Last 7 days
|
|
239
|
+
teams-transcripts --list-transcripts 7
|
|
240
|
+
|
|
241
|
+
# Last 14 days, specific tenant, JSON output to file
|
|
242
|
+
teams-transcripts --list-transcripts 14 --tenant choreograph.com --format json --output meetings.json
|
|
243
|
+
|
|
244
|
+
# Last 30 days, table output
|
|
245
|
+
teams-transcripts --list-transcripts 30
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Text output:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
Date Subject T R ID Call ID
|
|
252
|
+
---------- ------------------------------------------------ - - ------------------------------------ ------------------------------------
|
|
253
|
+
2026-04-29 Campaign Connect Leads Y Y f0a4b1c3-228f-4f24-8efb-6736215a3072 aae0280e-5b7e-473d-8a4b-99de3cf74a08
|
|
254
|
+
2026-04-29 OMS Forum Y Y 3f9c903d-339a-4be7-a796-c13001c6af01 b12f445a-6c1e-4e2d-9f01-4e8a6b7c9d0e
|
|
255
|
+
2026-04-29 Data connectors development Y Y 6d5d5a9f-357a-4f3c-a2be-d489454f6762 c23e556b-7d2f-5f3e-a012-5f9b7c8d0e1f
|
|
256
|
+
2026-04-29 UAP mid-Weekly Y Y 39158372-e829-454e-ad92-3d8812527aa9 d34f667c-8e3g-6g4f-b123-6g0c8d9e1f2g
|
|
257
|
+
2026-04-29 Dive into data for Data Connectors Y Y 67c13927-9b23-4f30-ae67-a489b3842994 e45g778d-9f4h-7h5g-c234-7h1d9e0f2g3h
|
|
258
|
+
2026-04-29 UAP / Trafficking / architecture Y Y 589a1e41-3361-4231-8fc0-582e42fe87c3 f56h889e-0g5i-8i6h-d345-8i2e0f1g3h4i
|
|
259
|
+
2026-04-28 Optimization Tech leads Fortnightly Sync session Y Y 2b4c1f5c-636d-4d78-a087-c2cbf3887fd8 g67i990f-1h6j-9j7i-e456-9j3f1g2h4i5j
|
|
260
|
+
2026-04-27 Campaign Connect Leads Y Y f0a4b1c3-228f-4f24-8efb-6736215a3072 h78j001g-2i7k-0k8j-f567-0k4g2h3i5j6k
|
|
261
|
+
|
|
262
|
+
8 meeting(s) found.
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Columns: **Date** (ISO), **Subject** (meeting title, max 48 chars),
|
|
266
|
+
**T** (has transcript: Y/N), **R** (has recording: Y/N),
|
|
267
|
+
**ID** (meeting UUID -- use with `--meeting-id` to download),
|
|
268
|
+
**Call ID** (per-occurrence identifier -- use with `--call-id` for
|
|
269
|
+
unambiguous targeting of recurring meetings; shown only when MCPS returns
|
|
270
|
+
call IDs).
|
|
271
|
+
|
|
272
|
+
Note: for recurring meetings (e.g. "Campaign Connect Leads" above), the
|
|
273
|
+
**ID** column shows the same UUID for all occurrences (they share a single
|
|
274
|
+
chat thread). The **Call ID** column uniquely identifies each occurrence.
|
|
275
|
+
|
|
276
|
+
JSON output (`--format json`):
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
[
|
|
280
|
+
{
|
|
281
|
+
"subject": "Campaign Connect Leads",
|
|
282
|
+
"date": "2026-04-29",
|
|
283
|
+
"thread_id": "19:meeting_ZjBhNGIxYzMtMjI4Zi00ZjI0LThlZmItNjczNjIxNWEzMDcy@thread.v2",
|
|
284
|
+
"call_id": null,
|
|
285
|
+
"ical_uid": "040000008200E00074C5B7101A82E008...",
|
|
286
|
+
"has_transcript": true,
|
|
287
|
+
"has_recording": true,
|
|
288
|
+
"transcript_url": "https://tenant-my.sharepoint.com/personal/...transcripts/.../content",
|
|
289
|
+
"recording_url": "https://tenant-my.sharepoint.com/personal/.../Meeting%20Recording.mp4"
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The JSON output includes SharePoint URLs for transcripts/recordings when MCPS
|
|
295
|
+
returns them. These URLs can be used for direct download with appropriate
|
|
296
|
+
authentication.
|
|
297
|
+
|
|
298
|
+
The legacy entry point `transcript-download` (backed by the original monolith
|
|
299
|
+
in `legacy/transcript_download.py`) is still registered but is no longer the
|
|
300
|
+
primary interface.
|
|
301
|
+
|
|
302
|
+
### Required arguments (download mode)
|
|
303
|
+
|
|
304
|
+
One of the following is required (mutually exclusive):
|
|
305
|
+
|
|
306
|
+
| Argument | Description |
|
|
307
|
+
|----------|-------------|
|
|
308
|
+
| `--meeting NAME` | Meeting title as it appears in Teams search results |
|
|
309
|
+
| `--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`. |
|
|
310
|
+
| `--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`. |
|
|
311
|
+
|
|
312
|
+
### Optional arguments
|
|
313
|
+
|
|
314
|
+
| Argument | Default | Description |
|
|
315
|
+
|----------|---------|-------------|
|
|
316
|
+
| `--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). |
|
|
317
|
+
| `--output PATH` | stdout | Output file path. When omitted, content is written to stdout. File format is determined by `--format`. |
|
|
318
|
+
| `--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. |
|
|
319
|
+
| `--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`. |
|
|
320
|
+
| `--quiet` / `-q` | off | Suppress all diagnostic output on stderr. Errors and `--format json` output are still emitted. |
|
|
321
|
+
| `--verbose` / `-v` | off | Emit additional diagnostic detail on stderr. Mutually exclusive with `--quiet`. |
|
|
322
|
+
| `--gcp-project PROJECT_ID` | `$GOOGLE_CLOUD_PROJECT` | GCP project for Vertex AI |
|
|
323
|
+
| `--port PORT` | `9333` | CDP remote debugging port. Must be between 1 and 65535. |
|
|
324
|
+
| `--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. |
|
|
325
|
+
| `--gcp-region REGION` | `$CLOUD_ML_REGION` or `global` | Vertex AI region |
|
|
326
|
+
| `--model MODEL_ID` | `$MODEL_ID` or `claude-haiku-4-5` | Anthropic model for the agent |
|
|
327
|
+
| `--adc PATH` | `$GOOGLE_ADC` or `~/.config/gcloud/...` | Path to ADC JSON file |
|
|
328
|
+
| `--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. |
|
|
329
|
+
| `--list-tenants` | off | List all available Azure AD tenants and exit. Incompatible with `--meeting`, `--meeting-id`, `--call-id`, and `--datetime`. See [Tenant selection](#tenant-selection) below. |
|
|
330
|
+
| `--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](#list-meetings-with-transcripts) below. |
|
|
331
|
+
| `--transcript {yes,no}` | `yes` | Include transcript cues in output. When `no`, the transcript download is skipped and output is forced to JSON. |
|
|
332
|
+
| `--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. |
|
|
333
|
+
| `--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). |
|
|
334
|
+
|
|
335
|
+
### Environment variables
|
|
336
|
+
|
|
337
|
+
All optional arguments can be set via environment variables as fallbacks:
|
|
338
|
+
|
|
339
|
+
| Variable | Used by |
|
|
340
|
+
|----------|---------|
|
|
341
|
+
| `GOOGLE_CLOUD_PROJECT` | `--gcp-project` (required if flag not provided) |
|
|
342
|
+
| `CLOUD_ML_REGION` | `--gcp-region` |
|
|
343
|
+
| `MODEL_ID` | `--model` |
|
|
344
|
+
| `GOOGLE_ADC` | `--adc` |
|
|
345
|
+
| `TEAMS_TENANT` | `--tenant` |
|
|
346
|
+
|
|
347
|
+
### Examples
|
|
348
|
+
|
|
349
|
+
Download a single-occurrence meeting transcript:
|
|
350
|
+
|
|
351
|
+
```sh
|
|
352
|
+
uv run teams-transcripts \
|
|
353
|
+
--meeting 'Project Kickoff' \
|
|
354
|
+
--datetime 2026-04-20 \
|
|
355
|
+
--output ./transcripts/kickoff.vtt \
|
|
356
|
+
--gcp-project my-vertex-project
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Download a recurring meeting (the script selects the correct occurrence) based upon the datetime:
|
|
360
|
+
|
|
361
|
+
```sh
|
|
362
|
+
uv run teams-transcripts \
|
|
363
|
+
--meeting 'Weekly Standup' \
|
|
364
|
+
--datetime 2026-04-14 \
|
|
365
|
+
--output ./transcripts/standup-2026-04-14.vtt
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Disambiguate two meetings with the same name on the same day by adding a time:
|
|
369
|
+
|
|
370
|
+
```sh
|
|
371
|
+
uv run teams-transcripts \
|
|
372
|
+
--meeting 'Weekly Standup' \
|
|
373
|
+
--datetime 2026-04-14T14:00 \
|
|
374
|
+
--output ./transcripts/standup-afternoon.vtt
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Pipe VTT to stdout (no file written):
|
|
378
|
+
|
|
379
|
+
```sh
|
|
380
|
+
uv run teams-transcripts \
|
|
381
|
+
--meeting 'Project Kickoff' \
|
|
382
|
+
--datetime 2026-04-20 | less
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Download by meeting ID (from `--list-transcripts` output):
|
|
386
|
+
|
|
387
|
+
```sh
|
|
388
|
+
uv run teams-transcripts \
|
|
389
|
+
--meeting-id 2b4c1f5c-636d-4d78-a087-c2cbf3887fd8 \
|
|
390
|
+
--output ./transcripts/meeting.vtt
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
This eliminates ambiguity when multiple meetings share the same name. The UUID
|
|
394
|
+
is shown in the `ID` column of `--list-transcripts` table output. The meeting
|
|
395
|
+
date is resolved automatically from the UUID via the MCPS API.
|
|
396
|
+
|
|
397
|
+
Download a specific occurrence of a recurring meeting by call ID:
|
|
398
|
+
|
|
399
|
+
```sh
|
|
400
|
+
uv run teams-transcripts \
|
|
401
|
+
--call-id aae0280e-5b7e-473d-8a4b-99de3cf74a08 \
|
|
402
|
+
--output ./transcripts/standup-2026-04-29.vtt
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The call ID is shown in the `Call ID` column of `--list-transcripts` table
|
|
406
|
+
output. Unlike the meeting UUID (which is shared across all occurrences of a
|
|
407
|
+
recurring series), the call ID uniquely identifies a specific occurrence. This
|
|
408
|
+
is the preferred method for recurring meetings.
|
|
409
|
+
|
|
410
|
+
Get structured JSON output to a file (nothing on stdout):
|
|
411
|
+
|
|
412
|
+
```sh
|
|
413
|
+
uv run teams-transcripts \
|
|
414
|
+
--meeting 'Project Kickoff' \
|
|
415
|
+
--datetime 2026-04-20 \
|
|
416
|
+
--output ./kickoff.json --format json
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The JSON file includes the full structured transcript as a `"transcript"` array
|
|
420
|
+
of cue objects:
|
|
421
|
+
|
|
422
|
+
```json
|
|
423
|
+
{
|
|
424
|
+
"status": "ok",
|
|
425
|
+
"meeting": "Project Kickoff",
|
|
426
|
+
"start": "2026-04-20T10:00+01:00",
|
|
427
|
+
"end": "2026-04-20T11:00+01:00",
|
|
428
|
+
"organiser": {"name": "Alice", "email": "alice@example.com"},
|
|
429
|
+
"invited": [
|
|
430
|
+
{"name": "Alice", "email": "alice@example.com", "rsvp_status": "organizer"},
|
|
431
|
+
{"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"},
|
|
432
|
+
{"name": "Charlie", "email": "charlie@example.com", "rsvp_status": "tentative"}
|
|
433
|
+
],
|
|
434
|
+
"speakers_list": [
|
|
435
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
436
|
+
{"name": "Bob", "email": "bob@example.com"}
|
|
437
|
+
],
|
|
438
|
+
"attendees": [
|
|
439
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
440
|
+
{"name": "Bob", "email": "bob@example.com"},
|
|
441
|
+
{"name": "Charlie", "email": "charlie@example.com"}
|
|
442
|
+
],
|
|
443
|
+
"transcript": [
|
|
444
|
+
{
|
|
445
|
+
"id": 1, "start": "00:00:03.000", "end": "00:00:25.000",
|
|
446
|
+
"start_sec": 3.0, "end_sec": 25.0,
|
|
447
|
+
"speaker": "Alice",
|
|
448
|
+
"text": "So I do have this concept of account sets."
|
|
449
|
+
}
|
|
450
|
+
],
|
|
451
|
+
"chat": [
|
|
452
|
+
{
|
|
453
|
+
"author": "Bob",
|
|
454
|
+
"text": "Here is the link: https://example.com/doc",
|
|
455
|
+
"reactions": [{"type": "like", "count": 2, "users": ["Alice", "Charlie"]}],
|
|
456
|
+
"offset": "00:05:12.000"
|
|
457
|
+
}
|
|
458
|
+
],
|
|
459
|
+
"parts": 1,
|
|
460
|
+
"cues": 342,
|
|
461
|
+
"speakers": {"Alice": 180, "Bob": 162},
|
|
462
|
+
"file_size": 48210,
|
|
463
|
+
"chat_messages": 2
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Get structured JSON on stdout (agent-friendly, no file):
|
|
468
|
+
|
|
469
|
+
```sh
|
|
470
|
+
uv run teams-transcripts \
|
|
471
|
+
--meeting 'Project Kickoff' \
|
|
472
|
+
--datetime 2026-04-20 --format json
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The same schema is written to stdout. With `--output`, the JSON is written to
|
|
476
|
+
the file and stdout is empty.
|
|
477
|
+
|
|
478
|
+
Dry-run to check a meeting exists without downloading:
|
|
479
|
+
|
|
480
|
+
```sh
|
|
481
|
+
uv run teams-transcripts \
|
|
482
|
+
--meeting 'Weekly Standup' \
|
|
483
|
+
--datetime 2026-04-14 --dry-run --format json
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Quiet mode for cron or agent use (suppress diagnostic logs):
|
|
487
|
+
|
|
488
|
+
```sh
|
|
489
|
+
uv run teams-transcripts \
|
|
490
|
+
--meeting 'Weekly Standup' \
|
|
491
|
+
--datetime 2026-04-14 \
|
|
492
|
+
--output ./standup.vtt -q
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Use a specific model and region:
|
|
496
|
+
|
|
497
|
+
```sh
|
|
498
|
+
uv run teams-transcripts \
|
|
499
|
+
--meeting 'Quarterly Review' \
|
|
500
|
+
--datetime 2026-03-31 \
|
|
501
|
+
--output ./review.vtt \
|
|
502
|
+
--model claude-sonnet-4-20250514 \
|
|
503
|
+
--gcp-region us-east5
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Tenant selection
|
|
507
|
+
|
|
508
|
+
When your Microsoft account has access to multiple Azure AD tenants
|
|
509
|
+
(organisations), you can list them and target a specific one:
|
|
510
|
+
|
|
511
|
+
```sh
|
|
512
|
+
# List all available tenants
|
|
513
|
+
uv run teams-transcripts --list-tenants
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Text output:
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
Available tenants (* = current):
|
|
520
|
+
|
|
521
|
+
NAME DOMAIN TENANT ID
|
|
522
|
+
* ABC Corp abccorp.onmicrosoft.com 86cbe1bb-213f-4271-b174-bd59d03c87a3
|
|
523
|
+
Contoso Ltd contoso.onmicrosoft.com 2b755fa1-23d1-48f3-98fc-6fdc1dc48d69
|
|
524
|
+
|
|
525
|
+
Use --tenant NAME_OR_DOMAIN to select a tenant for download.
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
JSON output:
|
|
529
|
+
|
|
530
|
+
```sh
|
|
531
|
+
uv run teams-transcripts --list-tenants --format json
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
```json
|
|
535
|
+
[
|
|
536
|
+
{
|
|
537
|
+
"tenantId": "86cbe1bb-213f-4271-b174-bd59d03c87a3",
|
|
538
|
+
"tenantName": "ABC Corp",
|
|
539
|
+
"domain": "abccorp.onmicrosoft.com",
|
|
540
|
+
"current": true
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"tenantId": "2b755fa1-23d1-48f3-98fc-6fdc1dc48d69",
|
|
544
|
+
"tenantName": "Contoso Ltd",
|
|
545
|
+
"domain": "contoso.onmicrosoft.com",
|
|
546
|
+
"current": false
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Write the tenant list to a file:
|
|
552
|
+
|
|
553
|
+
```sh
|
|
554
|
+
uv run teams-transcripts --list-tenants --output tenants.json --format json
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Download a transcript from a specific tenant (auto-switches if needed):
|
|
558
|
+
|
|
559
|
+
```sh
|
|
560
|
+
uv run teams-transcripts \
|
|
561
|
+
--meeting 'Weekly Standup' \
|
|
562
|
+
--datetime 2026-04-14 \
|
|
563
|
+
--output ./standup.vtt \
|
|
564
|
+
--tenant contoso.onmicrosoft.com
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
When `--tenant` is specified, the tool:
|
|
568
|
+
|
|
569
|
+
1. Detects which tenant Teams is currently signed into
|
|
570
|
+
2. Lists all available tenants for the signed-in user
|
|
571
|
+
3. If the current tenant matches, proceeds normally
|
|
572
|
+
4. If the current tenant does not match but the requested tenant is in the
|
|
573
|
+
list, kills Teams and relaunches it on the correct tenant automatically
|
|
574
|
+
5. If the requested tenant is not in the available list at all, exits with
|
|
575
|
+
code 6 (`TENANT_NOT_FOUND`)
|
|
576
|
+
|
|
577
|
+
The `--tenant` value is matched case-insensitively against both the tenant
|
|
578
|
+
name and the primary domain name from the tenant list.
|
|
579
|
+
|
|
580
|
+
### Component selection
|
|
581
|
+
|
|
582
|
+
By default all three components are extracted: transcript, chat, and metadata.
|
|
583
|
+
Use the `--transcript`, `--chat`, and `--metadata` flags to select which
|
|
584
|
+
components to include. At least one must be `yes`, except with `--dry-run`,
|
|
585
|
+
where all three may be `no` for a pure meeting-exists check.
|
|
586
|
+
|
|
587
|
+
Extract only chat messages (no transcript download, faster):
|
|
588
|
+
|
|
589
|
+
```sh
|
|
590
|
+
uv run teams-transcripts \
|
|
591
|
+
--meeting 'Weekly Standup' \
|
|
592
|
+
--datetime 2026-04-14 \
|
|
593
|
+
--transcript no --metadata no --format json
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Extract only participant metadata:
|
|
597
|
+
|
|
598
|
+
```sh
|
|
599
|
+
uv run teams-transcripts \
|
|
600
|
+
--meeting 'Weekly Standup' \
|
|
601
|
+
--datetime 2026-04-14 \
|
|
602
|
+
--transcript no --chat no --format json
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Extract chat and metadata without the transcript:
|
|
606
|
+
|
|
607
|
+
```sh
|
|
608
|
+
uv run teams-transcripts \
|
|
609
|
+
--meeting 'Weekly Standup' \
|
|
610
|
+
--datetime 2026-04-14 \
|
|
611
|
+
--transcript no --format json
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
When `--transcript no` is set, the output format is forced to JSON (VTT without
|
|
615
|
+
transcript cues is meaningless). Explicitly passing `--format vtt` alongside
|
|
616
|
+
`--transcript no` is an error.
|
|
617
|
+
|
|
618
|
+
When `--chat no --metadata yes` is set, chat messages are omitted from output,
|
|
619
|
+
but Event/Call messages may still be read internally to build attendee metadata.
|
|
620
|
+
|
|
621
|
+
The JSON output is composable: each component adds only its own fields. When
|
|
622
|
+
all three are `yes` (the default), the output is identical to the standard
|
|
623
|
+
full-extraction JSON schema.
|
|
624
|
+
|
|
625
|
+
## MCP server
|
|
626
|
+
|
|
627
|
+
The package includes a [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
628
|
+
(MCP) server that exposes the extraction tools over STDIO. This allows any
|
|
629
|
+
MCP-compatible client (Claude Desktop, Cursor, OpenCode, etc.) to call the
|
|
630
|
+
tools directly.
|
|
631
|
+
|
|
632
|
+
### Running the MCP server
|
|
633
|
+
|
|
634
|
+
```sh
|
|
635
|
+
# STDIO transport (default, for subprocess spawning by MCP clients)
|
|
636
|
+
teams-transcripts-mcp
|
|
637
|
+
|
|
638
|
+
# SSE transport on a specific port (for network MCP clients)
|
|
639
|
+
teams-transcripts-mcp --port 8080
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Or with `uv run`:
|
|
643
|
+
|
|
644
|
+
```sh
|
|
645
|
+
uv run teams-transcripts-mcp
|
|
646
|
+
uv run teams-transcripts-mcp --port 8080
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
Or via the Python module:
|
|
650
|
+
|
|
651
|
+
```sh
|
|
652
|
+
uv run python -m teams_transcripts.mcp_server
|
|
653
|
+
uv run python -m teams_transcripts.mcp_server --port 8080
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
When `--port` is omitted, the server communicates over STDIO (the standard
|
|
657
|
+
MCP transport for local subprocess spawning). When `--port` is specified,
|
|
658
|
+
the server runs as an SSE HTTP server on that port, accessible over the
|
|
659
|
+
network.
|
|
660
|
+
|
|
661
|
+
The server inherits environment variables for GCP configuration
|
|
662
|
+
(see [Environment variables](#environment-variables)).
|
|
663
|
+
|
|
664
|
+
### Available tools
|
|
665
|
+
|
|
666
|
+
| Tool | Description |
|
|
667
|
+
|------|-------------|
|
|
668
|
+
| `restart_teams` | Safely relaunch Teams with CDP flags and wait for readiness |
|
|
669
|
+
| `list_tenants` | List available Azure AD tenants |
|
|
670
|
+
| `list_transcripts` | Discover meetings with transcripts (last N days) |
|
|
671
|
+
| `download` | **Unified entry point** -- accepts UUID or name+date, validates args |
|
|
672
|
+
| `download_by_meeting_id` | Download transcript + chat + metadata by UUID |
|
|
673
|
+
| `download_transcript_by_meeting_id` | Download only transcript by UUID |
|
|
674
|
+
| `download_chat_by_meeting_id` | Download only chat messages by UUID |
|
|
675
|
+
| `download_metadata_by_meeting_id` | Download only participant metadata by UUID |
|
|
676
|
+
| `download_all` | Download transcript + chat + metadata by name + date |
|
|
677
|
+
| `download_transcript` | Download only the transcript (WebVTT cues) by name + date |
|
|
678
|
+
| `download_chat` | Download only chat messages by name + date |
|
|
679
|
+
| `download_meeting_metadata` | Download only participant metadata by name + date |
|
|
680
|
+
|
|
681
|
+
**Discovery tools** (`list_tenants`, `list_transcripts`) accept an optional
|
|
682
|
+
`port` parameter and return JSON arrays.
|
|
683
|
+
|
|
684
|
+
**Unified download tool** (`download`) is the recommended entry point for
|
|
685
|
+
AI agents. It accepts either `meeting_id` (preferred) or `meeting` + `date`,
|
|
686
|
+
validates argument combinations before launching the subprocess, and supports
|
|
687
|
+
`dry_run=True` to verify a meeting can be found without transcript download,
|
|
688
|
+
output chat extraction, Event/Call attendance extraction, or Details tracking
|
|
689
|
+
extraction. In dry-run mode, all three component flags may be `False` for a
|
|
690
|
+
pure meeting-exists check.
|
|
691
|
+
|
|
692
|
+
**UUID-based download tools** (`download_by_meeting_id`, etc.) accept a
|
|
693
|
+
`meeting_id` (UUID from `list_transcripts` output), optional `tenant`,
|
|
694
|
+
`port`, and `dry_run`. The meeting subject and date are resolved
|
|
695
|
+
automatically from the UUID.
|
|
696
|
+
|
|
697
|
+
**Name-based download tools** (`download_all`, etc.) accept `meeting`
|
|
698
|
+
(required), `date` (required), and optional `time`, `tenant`, `port`, and
|
|
699
|
+
`dry_run` parameters.
|
|
700
|
+
|
|
701
|
+
#### Recommended workflow
|
|
702
|
+
|
|
703
|
+
```
|
|
704
|
+
1. list_transcripts(days=7) → discover meetings, get UUIDs
|
|
705
|
+
2. download(meeting_id=uuid) → extract transcript by UUID (preferred)
|
|
706
|
+
OR download(meeting_id=uuid, dry_run=True) → verify meeting exists first
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Or for a known meeting:
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
1. download(meeting="Weekly Standup", date="2026-04-29")
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### Configuration for Claude Desktop
|
|
716
|
+
|
|
717
|
+
Add to your `claude_desktop_config.json`:
|
|
718
|
+
|
|
719
|
+
```json
|
|
720
|
+
{
|
|
721
|
+
"mcpServers": {
|
|
722
|
+
"teams-transcripts": {
|
|
723
|
+
"command": "/path/to/teams-transcripts/bin/teams-transcripts-mcp",
|
|
724
|
+
"env": {
|
|
725
|
+
"GOOGLE_CLOUD_PROJECT": "your-gcp-project"
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Or using `uv run`:
|
|
733
|
+
|
|
734
|
+
```json
|
|
735
|
+
{
|
|
736
|
+
"mcpServers": {
|
|
737
|
+
"teams-transcripts": {
|
|
738
|
+
"command": "uv",
|
|
739
|
+
"args": ["run", "--directory", "/path/to/teams-transcripts", "teams-transcripts-mcp"],
|
|
740
|
+
"env": {
|
|
741
|
+
"GOOGLE_CLOUD_PROJECT": "your-gcp-project"
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
### Architecture
|
|
749
|
+
|
|
750
|
+
Each MCP tool shells out to the `teams-transcripts` CLI with `--format json
|
|
751
|
+
--quiet` and returns the parsed JSON response. The subprocess approach provides:
|
|
752
|
+
|
|
753
|
+
- Process isolation (Teams CDP locks are per-process)
|
|
754
|
+
- Clean signal handling (the child manages its own SIGINT/SIGTERM)
|
|
755
|
+
- No import-time side effects in the MCP server process
|
|
756
|
+
|
|
757
|
+
Discovery tools (`list_tenants`, `list_transcripts`) return JSON arrays.
|
|
758
|
+
Download tools return a JSON object with the extraction results. UUID-based
|
|
759
|
+
tools use `--meeting-id` internally; name-based tools use `--meeting` +
|
|
760
|
+
`--datetime`.
|
|
761
|
+
|
|
762
|
+
### Safe Teams restarts
|
|
763
|
+
|
|
764
|
+
Every automatic Teams restart checks for a possible live call immediately
|
|
765
|
+
before killing the app. The check samples CoreAudio activity for the Teams app
|
|
766
|
+
and its helper processes three times, then checks the macOS Accessibility tree
|
|
767
|
+
for a visible Leave or Hang-up control. The result is `active`, `inactive`, or
|
|
768
|
+
`unknown`; both `active` and `unknown` block automatic CDP recovery and tenant
|
|
769
|
+
switching.
|
|
770
|
+
|
|
771
|
+
Blocked CLI flows exit with code 2 and the machine-stable `RESTART_BLOCKED`
|
|
772
|
+
error. MCP callers receive the same error through download/listing tools. The
|
|
773
|
+
`restart_teams` MCP tool instead returns a structured result containing
|
|
774
|
+
`status`, `restarted`, `cdp_ready`, `port`, `call_state`, and `forced`. A block
|
|
775
|
+
also includes `reason: active_call` or `reason: call_state_unknown`.
|
|
776
|
+
|
|
777
|
+
The CLI flag `--force-restart` overrides the safeguard for any automatic CDP
|
|
778
|
+
recovery or tenant switch required by that invocation. The MCP equivalent is
|
|
779
|
+
`restart_teams(force=True)`. Both may interrupt a live call and must be used
|
|
780
|
+
only when interruption is explicitly intended. Without an override, downloads,
|
|
781
|
+
listing, tenant switching, and recovery continue to fail closed.
|
|
782
|
+
|
|
783
|
+
## Output format
|
|
784
|
+
|
|
785
|
+
### VTT file structure
|
|
786
|
+
|
|
787
|
+
The output is a standard [WebVTT](https://www.w3.org/TR/webvtt1/) file
|
|
788
|
+
containing three sections:
|
|
789
|
+
|
|
790
|
+
**1. Meeting metadata** -- a `NOTE Meeting Metadata` comment block at the top:
|
|
791
|
+
|
|
792
|
+
```
|
|
793
|
+
WEBVTT
|
|
794
|
+
|
|
795
|
+
NOTE Meeting Metadata
|
|
796
|
+
Meeting: Weekly Standup
|
|
797
|
+
Organiser: Alice <alice@example.com>
|
|
798
|
+
Invited: Alice, Bob, Charlie
|
|
799
|
+
Speakers: Alice, Bob
|
|
800
|
+
Attendees: Alice, Bob, Charlie
|
|
801
|
+
Start: 2026-04-14T10:00+01:00
|
|
802
|
+
End: 2026-04-14T10:30+01:00
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
For larger meetings (more than 10 invited or attendees), counts are shown
|
|
806
|
+
instead of names, with an RSVP breakdown when tracking data is available:
|
|
807
|
+
|
|
808
|
+
```
|
|
809
|
+
NOTE Meeting Metadata
|
|
810
|
+
Meeting: Q1 2026 All Hands
|
|
811
|
+
Organiser: Suman Landa <suman.landa@example.com>
|
|
812
|
+
Invited: 90 members (50 accepted, 5 tentative, 2 declined, 33 no response)
|
|
813
|
+
Speakers: Alice, Bob, Charlie, Dave, Eve, Fran
|
|
814
|
+
Attendees: 77 participants
|
|
815
|
+
Start: 2026-04-01T09:00+01:00
|
|
816
|
+
End: 2026-04-01T10:30+01:00
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Fields:
|
|
820
|
+
|
|
821
|
+
- **Meeting**: meeting title.
|
|
822
|
+
- **Organiser**: meeting creator with email in `Name <email>` format.
|
|
823
|
+
- **Invited**: all members on the meeting chat thread. Listed by name
|
|
824
|
+
when 10 or fewer; shown as a count with RSVP summary when more than 10.
|
|
825
|
+
RSVP statuses are: accepted, tentative, declined, no response.
|
|
826
|
+
- **Speakers**: members who spoke during the meeting (from Recap).
|
|
827
|
+
- **Attendees**: members who joined the call (from Event/Call partlist).
|
|
828
|
+
May be incomplete for large meetings due to server-side truncation.
|
|
829
|
+
- **Start** / **End**: ISO 8601 timestamps with timezone offset.
|
|
830
|
+
|
|
831
|
+
**2. Chat messages** -- a `NOTE Chat` comment block containing a JSON array of
|
|
832
|
+
messages sent during the meeting:
|
|
833
|
+
|
|
834
|
+
```
|
|
835
|
+
NOTE Chat
|
|
836
|
+
[
|
|
837
|
+
{
|
|
838
|
+
"author": "Bob",
|
|
839
|
+
"text": "Link to the design doc: https://example.com/doc",
|
|
840
|
+
"reactions": [
|
|
841
|
+
{"type": "like", "count": 2, "users": ["Alice", "Charlie"]}
|
|
842
|
+
],
|
|
843
|
+
"offset": "00:12:34.567"
|
|
844
|
+
}
|
|
845
|
+
]
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Each message includes: author name, plain text (HTML stripped, links
|
|
849
|
+
preserved), reactions (with per-user attribution where resolvable), and an
|
|
850
|
+
`offset` from meeting start in `HH:MM:SS.mmm` format where computable. The
|
|
851
|
+
`offset` field is `null` when it cannot be computed. The `reactions` field is
|
|
852
|
+
`null` when there are no reactions.
|
|
853
|
+
|
|
854
|
+
**3. Transcript cues** -- standard WebVTT cues with speaker identification:
|
|
855
|
+
|
|
856
|
+
```
|
|
857
|
+
1
|
|
858
|
+
00:00:03.000 --> 00:00:25.000
|
|
859
|
+
<v Alice>So I do have this concept of account sets. It's called
|
|
860
|
+
platform account sets in the code base, which basically...</v>
|
|
861
|
+
|
|
862
|
+
2
|
|
863
|
+
00:00:27.000 --> 00:00:48.000
|
|
864
|
+
<v Bob>Looks something like this. For example, this is our dev
|
|
865
|
+
accounts.</v>
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
Each cue contains a sequential number, start and end timestamps in
|
|
869
|
+
`HH:MM:SS.mmm` format, the speaker name in a `<v Name>` voice span, and the
|
|
870
|
+
spoken text.
|
|
871
|
+
|
|
872
|
+
For multi-part meetings (where Teams splits the recording), cue timestamps are
|
|
873
|
+
offset so that Part 2 continues from where Part 1 ended, producing a single
|
|
874
|
+
continuous timeline.
|
|
875
|
+
|
|
876
|
+
### JSON output schemas
|
|
877
|
+
|
|
878
|
+
When `--format json` is used, a single JSON object is written to stdout (when
|
|
879
|
+
`--output` is omitted) or to the specified file (when `--output` is given).
|
|
880
|
+
In file mode, nothing is written to stdout.
|
|
881
|
+
|
|
882
|
+
**Success (all components)**:
|
|
883
|
+
|
|
884
|
+
```json
|
|
885
|
+
{
|
|
886
|
+
"status": "ok",
|
|
887
|
+
"meeting": "Weekly Standup",
|
|
888
|
+
"start": "2026-04-14T10:00+01:00",
|
|
889
|
+
"end": "2026-04-14T10:30+01:00",
|
|
890
|
+
"organiser": {"name": "Alice", "email": "alice@example.com"},
|
|
891
|
+
"invited": [
|
|
892
|
+
{"name": "Alice", "email": "alice@example.com", "rsvp_status": "organizer"},
|
|
893
|
+
{"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"},
|
|
894
|
+
{"name": "Charlie", "email": "charlie@example.com", "rsvp_status": "declined"}
|
|
895
|
+
],
|
|
896
|
+
"speakers_list": [
|
|
897
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
898
|
+
{"name": "Bob", "email": "bob@example.com"}
|
|
899
|
+
],
|
|
900
|
+
"attendees": [
|
|
901
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
902
|
+
{"name": "Bob", "email": "bob@example.com"},
|
|
903
|
+
{"name": "Charlie", "email": "charlie@example.com"}
|
|
904
|
+
],
|
|
905
|
+
"chat": [
|
|
906
|
+
{
|
|
907
|
+
"author": "Bob",
|
|
908
|
+
"text": "Here is the link: https://example.com/doc",
|
|
909
|
+
"reactions": [{"type": "like", "count": 2, "users": ["Alice", "Charlie"]}],
|
|
910
|
+
"offset": "00:05:12.000"
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
"author": "Alice",
|
|
914
|
+
"text": "Thanks Bob",
|
|
915
|
+
"reactions": null,
|
|
916
|
+
"offset": "00:05:45.000"
|
|
917
|
+
}
|
|
918
|
+
],
|
|
919
|
+
"transcript": [
|
|
920
|
+
{
|
|
921
|
+
"id": 1, "start": "00:00:03.000", "end": "00:00:25.000",
|
|
922
|
+
"start_sec": 3.0, "end_sec": 25.0,
|
|
923
|
+
"speaker": "Alice",
|
|
924
|
+
"text": "So I do have this concept of account sets."
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
"id": 2, "start": "00:00:27.000", "end": "00:00:48.000",
|
|
928
|
+
"start_sec": 27.0, "end_sec": 48.0,
|
|
929
|
+
"speaker": "Bob",
|
|
930
|
+
"text": "Looks something like this."
|
|
931
|
+
}
|
|
932
|
+
],
|
|
933
|
+
"parts": 1,
|
|
934
|
+
"cues": 342,
|
|
935
|
+
"speakers": {"Alice": 180, "Bob": 162},
|
|
936
|
+
"file_size": 48210,
|
|
937
|
+
"chat_messages": 2
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
**Dry-run** (`--dry-run --format json`):
|
|
942
|
+
|
|
943
|
+
```json
|
|
944
|
+
{
|
|
945
|
+
"status": "ok",
|
|
946
|
+
"dry_run": true,
|
|
947
|
+
"meeting": "Weekly Standup",
|
|
948
|
+
"start": "2026-04-14T10:00+01:00",
|
|
949
|
+
"end": "2026-04-14T10:30+01:00",
|
|
950
|
+
"organiser": {"name": "Alice", "email": "alice@example.com"},
|
|
951
|
+
"invited": [
|
|
952
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
953
|
+
{"name": "Bob", "email": "bob@example.com"},
|
|
954
|
+
{"name": "Charlie", "email": "charlie@example.com"}
|
|
955
|
+
],
|
|
956
|
+
"speakers_list": [
|
|
957
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
958
|
+
{"name": "Bob", "email": "bob@example.com"}
|
|
959
|
+
],
|
|
960
|
+
"parts": 1,
|
|
961
|
+
"speakers": {"Alice": 0, "Bob": 0}
|
|
962
|
+
}
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
Speaker counts are zero in dry-run mode (no transcript downloaded). The
|
|
966
|
+
`"cues"`, `"file_size"`, `"transcript"`, and `"chat_messages"` keys are
|
|
967
|
+
absent. `"attendees"` is absent in dry-run because Event/Call partlists are
|
|
968
|
+
only available after chat extraction, which dry-run skips.
|
|
969
|
+
|
|
970
|
+
**Error** (any non-zero exit):
|
|
971
|
+
|
|
972
|
+
```json
|
|
973
|
+
{
|
|
974
|
+
"status": "error",
|
|
975
|
+
"code": "MEETING_NOT_FOUND",
|
|
976
|
+
"message": "Could not find the target 2026-04-14 in any of the first 4 search results.",
|
|
977
|
+
"step": "step5"
|
|
978
|
+
}
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
The `"code"` field is a machine-stable error code (see `--help` for the full
|
|
982
|
+
list). The `"step"` field indicates which pipeline step failed (absent for
|
|
983
|
+
argument validation errors).
|
|
984
|
+
|
|
985
|
+
**Conditional keys**: `"end"` is absent if the meeting time text could not be
|
|
986
|
+
parsed. `"organiser"` is absent if the organiser could not be identified.
|
|
987
|
+
`"invited"` is absent if roster extraction failed entirely (popover
|
|
988
|
+
unavailable). `"speakers_list"` is absent if no speakers were found.
|
|
989
|
+
`"attendees"` is absent if no Event/Call partlist was available (e.g.
|
|
990
|
+
dry-run mode, or the meeting had no call join/leave events).
|
|
991
|
+
`"chat_messages"` is absent (not zero) when there are no chat messages.
|
|
992
|
+
`"rsvp_status"` is included on each invited member only when RSVP tracking
|
|
993
|
+
data was available; it is omitted (not empty) when the Details tab Tracking
|
|
994
|
+
panel could not be read.
|
|
995
|
+
|
|
996
|
+
### Component-specific JSON schemas
|
|
997
|
+
|
|
998
|
+
The JSON output is composable. Each component (`--transcript`, `--chat`,
|
|
999
|
+
`--metadata`) contributes only its own fields. Base fields (`status`,
|
|
1000
|
+
`meeting`, `start`, `end`) are always present regardless of which components
|
|
1001
|
+
are selected.
|
|
1002
|
+
|
|
1003
|
+
**Transcript only** (`--transcript yes --chat no --metadata no`):
|
|
1004
|
+
|
|
1005
|
+
```json
|
|
1006
|
+
{
|
|
1007
|
+
"status": "ok",
|
|
1008
|
+
"meeting": "Weekly Standup",
|
|
1009
|
+
"start": "2026-04-14T10:00+01:00",
|
|
1010
|
+
"end": "2026-04-14T10:30+01:00",
|
|
1011
|
+
"parts": 1,
|
|
1012
|
+
"cues": 342,
|
|
1013
|
+
"speakers": {"Alice": 180, "Bob": 162},
|
|
1014
|
+
"file_size": 48210,
|
|
1015
|
+
"transcript": [
|
|
1016
|
+
{
|
|
1017
|
+
"id": 1, "start": "00:00:03.000", "end": "00:00:25.000",
|
|
1018
|
+
"start_sec": 3.0, "end_sec": 25.0,
|
|
1019
|
+
"speaker": "Alice",
|
|
1020
|
+
"text": "So I do have this concept of account sets."
|
|
1021
|
+
}
|
|
1022
|
+
]
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Transcript-specific fields: `parts`, `cues`, `speakers` (name-to-cue-count
|
|
1027
|
+
map), `file_size`, and `transcript` (array of cue objects).
|
|
1028
|
+
|
|
1029
|
+
**Chat only** (`--transcript no --chat yes --metadata no`):
|
|
1030
|
+
|
|
1031
|
+
```json
|
|
1032
|
+
{
|
|
1033
|
+
"status": "ok",
|
|
1034
|
+
"meeting": "Weekly Standup",
|
|
1035
|
+
"start": "2026-04-14T10:00+01:00",
|
|
1036
|
+
"end": "2026-04-14T10:30+01:00",
|
|
1037
|
+
"chat": [
|
|
1038
|
+
{
|
|
1039
|
+
"author": "Bob",
|
|
1040
|
+
"text": "Here is the link: https://example.com/doc",
|
|
1041
|
+
"reactions": [{"type": "like", "count": 2, "users": ["Alice", "Charlie"]}],
|
|
1042
|
+
"offset": "00:05:12.000"
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
"author": "Alice",
|
|
1046
|
+
"text": "Thanks Bob",
|
|
1047
|
+
"reactions": null,
|
|
1048
|
+
"offset": "00:05:45.000"
|
|
1049
|
+
}
|
|
1050
|
+
]
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
Chat-specific fields: `chat` (array of message objects). Each message has:
|
|
1055
|
+
- `author` (string): display name of the sender
|
|
1056
|
+
- `text` (string): plain text content (HTML stripped, links preserved)
|
|
1057
|
+
- `reactions` (array or null): list of reaction objects, each with `type`,
|
|
1058
|
+
`count`, and `users` (array of display names)
|
|
1059
|
+
- `offset` (string or null): time offset from meeting start in `HH:MM:SS.mmm`
|
|
1060
|
+
format when computable
|
|
1061
|
+
|
|
1062
|
+
**Metadata only** (`--transcript no --chat no --metadata yes`):
|
|
1063
|
+
|
|
1064
|
+
```json
|
|
1065
|
+
{
|
|
1066
|
+
"status": "ok",
|
|
1067
|
+
"meeting": "Weekly Standup",
|
|
1068
|
+
"start": "2026-04-14T10:00+01:00",
|
|
1069
|
+
"end": "2026-04-14T10:30+01:00",
|
|
1070
|
+
"organiser": {"name": "Alice", "email": "alice@example.com"},
|
|
1071
|
+
"invited": [
|
|
1072
|
+
{"name": "Alice", "email": "alice@example.com", "rsvp_status": "organizer"},
|
|
1073
|
+
{"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"},
|
|
1074
|
+
{"name": "Charlie", "email": "charlie@example.com", "rsvp_status": "tentative"}
|
|
1075
|
+
],
|
|
1076
|
+
"speakers_list": [
|
|
1077
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
1078
|
+
{"name": "Bob", "email": "bob@example.com"}
|
|
1079
|
+
],
|
|
1080
|
+
"attendees": [
|
|
1081
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
1082
|
+
{"name": "Bob", "email": "bob@example.com"},
|
|
1083
|
+
{"name": "Charlie", "email": "charlie@example.com"}
|
|
1084
|
+
]
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Metadata-specific fields:
|
|
1089
|
+
- `organiser` (object): `{name, email}` of the meeting creator
|
|
1090
|
+
- `invited` (array): members on the chat thread, each with `name`, `email`,
|
|
1091
|
+
and optionally `rsvp_status` (one of: `organizer`, `accepted`, `tentative`,
|
|
1092
|
+
`declined`, `none`)
|
|
1093
|
+
- `speakers_list` (array): members who spoke, each with `name` and `email`
|
|
1094
|
+
- `attendees` (array): members who joined the call, each with `name` and
|
|
1095
|
+
`email`
|
|
1096
|
+
|
|
1097
|
+
**Chat + metadata** (`--transcript no --chat yes --metadata yes`):
|
|
1098
|
+
|
|
1099
|
+
```json
|
|
1100
|
+
{
|
|
1101
|
+
"status": "ok",
|
|
1102
|
+
"meeting": "Weekly Standup",
|
|
1103
|
+
"start": "2026-04-14T10:00+01:00",
|
|
1104
|
+
"end": "2026-04-14T10:30+01:00",
|
|
1105
|
+
"organiser": {"name": "Alice", "email": "alice@example.com"},
|
|
1106
|
+
"invited": [
|
|
1107
|
+
{"name": "Alice", "email": "alice@example.com", "rsvp_status": "accepted"},
|
|
1108
|
+
{"name": "Bob", "email": "bob@example.com", "rsvp_status": "accepted"}
|
|
1109
|
+
],
|
|
1110
|
+
"speakers_list": [
|
|
1111
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
1112
|
+
{"name": "Bob", "email": "bob@example.com"}
|
|
1113
|
+
],
|
|
1114
|
+
"attendees": [
|
|
1115
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
1116
|
+
{"name": "Bob", "email": "bob@example.com"}
|
|
1117
|
+
],
|
|
1118
|
+
"chat": [
|
|
1119
|
+
{
|
|
1120
|
+
"author": "Bob",
|
|
1121
|
+
"text": "Starting now",
|
|
1122
|
+
"reactions": null,
|
|
1123
|
+
"offset": "00:00:05.000"
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
}
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
**List tenants** (`--list-tenants --format json`):
|
|
1130
|
+
|
|
1131
|
+
```json
|
|
1132
|
+
[
|
|
1133
|
+
{
|
|
1134
|
+
"tenantId": "86cbe1bb-213f-4271-b174-bd59d03c87a3",
|
|
1135
|
+
"tenantName": "ABC Corp",
|
|
1136
|
+
"domain": "abccorp.onmicrosoft.com",
|
|
1137
|
+
"current": true
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
"tenantId": "2b755fa1-23d1-48f3-98fc-6fdc1dc48d69",
|
|
1141
|
+
"tenantName": "Contoso Ltd",
|
|
1142
|
+
"domain": "contoso.onmicrosoft.com",
|
|
1143
|
+
"current": false
|
|
1144
|
+
}
|
|
1145
|
+
]
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
### JSON field reference
|
|
1149
|
+
|
|
1150
|
+
| Field | Type | Present when | Description |
|
|
1151
|
+
|-------|------|-------------|-------------|
|
|
1152
|
+
| `status` | string | always | `"ok"` on success, `"error"` on failure |
|
|
1153
|
+
| `meeting` | string | always (success) | Meeting title |
|
|
1154
|
+
| `start` | string | always (success) | ISO 8601 meeting start time |
|
|
1155
|
+
| `end` | string | when parseable | ISO 8601 meeting end time |
|
|
1156
|
+
| `dry_run` | boolean | `--dry-run` mode | Always `true` when present |
|
|
1157
|
+
| `organiser` | object | `--metadata yes` | `{name, email}` |
|
|
1158
|
+
| `invited` | array | `--metadata yes` | `[{name, email, rsvp_status?}]` |
|
|
1159
|
+
| `speakers_list` | array | `--metadata yes` | `[{name, email}]` |
|
|
1160
|
+
| `attendees` | array | `--metadata yes` + attendance data available | `[{name, email}]` |
|
|
1161
|
+
| `chat` | array | `--chat yes` + messages exist | `[{author, text, reactions, offset}]` |
|
|
1162
|
+
| `chat_messages` | integer | all-components mode | Count of chat messages |
|
|
1163
|
+
| `parts` | integer | `--transcript yes` | Number of transcript parts |
|
|
1164
|
+
| `cues` | integer | `--transcript yes` | Total cue count |
|
|
1165
|
+
| `speakers` | object | `--transcript yes` | `{name: cue_count}` map |
|
|
1166
|
+
| `file_size` | integer | `--transcript yes` | VTT content size in bytes |
|
|
1167
|
+
| `transcript` | array | `--transcript yes` | `[{id, start, end, start_sec, end_sec, speaker, text}]` |
|
|
1168
|
+
| `code` | string | error | Machine-stable error code |
|
|
1169
|
+
| `message` | string | error | Human-readable error description |
|
|
1170
|
+
| `step` | string | error (runtime) | Pipeline step that failed |
|
|
1171
|
+
|
|
1172
|
+
## Recurring meetings
|
|
1173
|
+
|
|
1174
|
+
For recurring meetings, the script:
|
|
1175
|
+
|
|
1176
|
+
1. Navigates to the meeting chat group
|
|
1177
|
+
2. Opens the Recap tab and reads the occurrence dropdown
|
|
1178
|
+
3. If the default occurrence does not match the target date, opens the
|
|
1179
|
+
dropdown and scrolls through available dates
|
|
1180
|
+
4. Uses the LLM agent to identify and select the correct occurrence
|
|
1181
|
+
|
|
1182
|
+
Teams typically retains the most recent 5 or so occurrences in the dropdown.
|
|
1183
|
+
Older occurrences may no longer be available.
|
|
1184
|
+
|
|
1185
|
+
If the first search result leads to a chat group that does not contain the
|
|
1186
|
+
target date, the script automatically retries with the next search result
|
|
1187
|
+
(up to 4 attempts).
|
|
1188
|
+
|
|
1189
|
+
## Search modes
|
|
1190
|
+
|
|
1191
|
+
Teams uses two different search UI patterns depending on the context:
|
|
1192
|
+
|
|
1193
|
+
- **Panel mode**: results appear in a side panel with text-based matching.
|
|
1194
|
+
The script scores results by word overlap with the meeting name.
|
|
1195
|
+
- **Popup mode**: results appear in a dropdown overlay. The script uses the
|
|
1196
|
+
LLM agent to select the correct result from a screenshot.
|
|
1197
|
+
|
|
1198
|
+
The mode is auto-detected; no configuration is needed.
|
|
1199
|
+
|
|
1200
|
+
## Limitations
|
|
1201
|
+
|
|
1202
|
+
- **macOS only**: the script uses macOS-specific commands (`open -a`, `pkill`)
|
|
1203
|
+
to manage the Teams process.
|
|
1204
|
+
- **Occurrence dropdown depth**: Teams only retains recent occurrences (roughly
|
|
1205
|
+
the last 5). Older recurring meeting dates cannot be retrieved.
|
|
1206
|
+
- **Attendee list truncation**: the Event/Call `<partlist>` XML that Teams
|
|
1207
|
+
stores in chat messages is truncated server-side for large meetings. For
|
|
1208
|
+
example, a meeting declaring `count="77"` may only include 25 `<part>`
|
|
1209
|
+
elements. The attendee list in the output may therefore be incomplete.
|
|
1210
|
+
This is a Teams limitation with no client-side workaround.
|
|
1211
|
+
- **RSVP data is calendar responses, not actual attendance**: the tracking
|
|
1212
|
+
panel shows who accepted/declined/tentatively accepted the calendar
|
|
1213
|
+
invitation. It does not reflect who actually joined the call. Actual
|
|
1214
|
+
attendance is determined by the `<partlist>` (see above).
|
|
1215
|
+
- **RSVP extraction is soft-failure**: if the Details tab or Tracking panel
|
|
1216
|
+
is unavailable (e.g. the meeting type does not support tracking, or the
|
|
1217
|
+
UI layout changes), the pipeline continues without RSVP data. The
|
|
1218
|
+
`rsvp_status` field will be absent from JSON output in this case.
|
|
1219
|
+
- **Conference room accounts**: room/resource accounts appear with their
|
|
1220
|
+
system names (e.g. `UK-LON-1SB-05S19@oursystems.com`) as that is what
|
|
1221
|
+
Teams stores as the display name.
|
|
1222
|
+
|
|
1223
|
+
## Exit codes
|
|
1224
|
+
|
|
1225
|
+
| Code | Name | Meaning |
|
|
1226
|
+
|------|------|---------|
|
|
1227
|
+
| 0 | `EXIT_OK` | Success |
|
|
1228
|
+
| 1 | `EXIT_BAD_ARGS` | Invalid arguments or missing configuration |
|
|
1229
|
+
| 2 | `EXIT_CDP_UNAVAILABLE` | Teams/CDP not running or not responding |
|
|
1230
|
+
| 3 | `EXIT_MEETING_NOT_FOUND` | Meeting not found after all retries |
|
|
1231
|
+
| 4 | `EXIT_DOWNLOAD_FAILED` | Transcript download or permission error |
|
|
1232
|
+
| 5 | `EXIT_VERIFICATION_FAILED` | Output file is invalid |
|
|
1233
|
+
| 6 | `EXIT_TENANT_NOT_FOUND` | Requested tenant not in available tenants list |
|
|
1234
|
+
| 130 | `EXIT_INTERRUPTED` | Interrupted by SIGINT (Ctrl+C) |
|
|
1235
|
+
| 143 | `EXIT_TERMINATED` | Terminated by SIGTERM |
|
|
1236
|
+
|
|
1237
|
+
## Error codes
|
|
1238
|
+
|
|
1239
|
+
Each error includes a machine-stable code string used as a `[PREFIX]` on stderr
|
|
1240
|
+
and as the `"code"` field in JSON error output. These are stable identifiers
|
|
1241
|
+
suitable for programmatic matching.
|
|
1242
|
+
|
|
1243
|
+
| Code | Meaning |
|
|
1244
|
+
|------|---------|
|
|
1245
|
+
| `BAD_ARGS` | Invalid CLI arguments or missing configuration |
|
|
1246
|
+
| `CDP_UNAVAILABLE` | Teams/CDP not running or not responding |
|
|
1247
|
+
| `RESTART_BLOCKED` | Teams restart blocked by an active or unknown call state |
|
|
1248
|
+
| `NO_TARGETS` | No Teams page targets found via CDP |
|
|
1249
|
+
| `SEARCH_FAILED` | Search results did not appear in the UI |
|
|
1250
|
+
| `MEETING_NOT_FOUND` | Meeting not found in search results |
|
|
1251
|
+
| `RECAP_NOT_FOUND` | Recap tab not found after navigation |
|
|
1252
|
+
| `TRANSCRIPT_NOT_FOUND` | Transcript tab not found |
|
|
1253
|
+
| `IFRAME_NOT_FOUND` | No transcript iframe target among CDP targets |
|
|
1254
|
+
| `DOWNLOAD_FAILED` | VTT download failed |
|
|
1255
|
+
| `VERIFICATION_FAILED` | Output file content is invalid |
|
|
1256
|
+
| `TENANT_NOT_FOUND` | Requested tenant not in available tenants list |
|
|
1257
|
+
| `INTERRUPTED` | Process interrupted by SIGINT |
|
|
1258
|
+
| `TERMINATED` | Process terminated by SIGTERM |
|
|
1259
|
+
|
|
1260
|
+
## Agent and automation usage
|
|
1261
|
+
|
|
1262
|
+
The CLI is designed for use by both humans and automated agents. Key features
|
|
1263
|
+
for machine consumption:
|
|
1264
|
+
|
|
1265
|
+
- **stdout/stderr separation**: all diagnostic output goes to stderr. stdout
|
|
1266
|
+
carries only data -- VTT content in text mode, or a JSON object in JSON mode.
|
|
1267
|
+
When `--output` is given, nothing is written to stdout (both formats).
|
|
1268
|
+
Use `--quiet` to suppress diagnostics entirely.
|
|
1269
|
+
- **`--format json`**: emits a single JSON object. On success, the object
|
|
1270
|
+
contains meeting metadata, speaker counts, cue count, and a `"transcript"`
|
|
1271
|
+
array of structured cue objects. On failure, the object contains
|
|
1272
|
+
`"status": "error"`, a machine-stable `"code"` string, and a human message.
|
|
1273
|
+
- **Semantic exit codes**: agents can branch on the exit code without parsing
|
|
1274
|
+
error messages. See the table above.
|
|
1275
|
+
- **Machine-stable error codes**: each error includes a short code prefix in
|
|
1276
|
+
text mode (e.g. `[MEETING_NOT_FOUND]`) and a `"code"` field in JSON mode.
|
|
1277
|
+
Codes are listed in `--help` under "error codes".
|
|
1278
|
+
- **`--dry-run`**: finds the meeting, loads Recap only when needed, extracts
|
|
1279
|
+
safe roster metadata when requested, and detects parts without downloading
|
|
1280
|
+
the transcript, extracting chat/attendance data, or opening Details tracking.
|
|
1281
|
+
Useful for validation before committing to a full download.
|
|
1282
|
+
- **Signal handling**: SIGINT and SIGTERM are handled cleanly. The first
|
|
1283
|
+
signal sets a shutdown flag and the process exits at the next safe point
|
|
1284
|
+
(within 500 ms). A second signal force-exits immediately. JSON error
|
|
1285
|
+
output is emitted on signal exit when `--format json` is active.
|
|
1286
|
+
|
|
1287
|
+
### Output modes
|
|
1288
|
+
|
|
1289
|
+
| `--output` | `--format` | `--dry-run` | stdout | file |
|
|
1290
|
+
|------------|------------|-------------|--------|------|
|
|
1291
|
+
| PATH | vtt | no | (empty) | VTT |
|
|
1292
|
+
| PATH | json | no | (empty) | JSON |
|
|
1293
|
+
| omitted | vtt | no | raw VTT content | (none) |
|
|
1294
|
+
| omitted | json | no | structured JSON | (none) |
|
|
1295
|
+
| omitted | vtt | yes | (empty) | (none) |
|
|
1296
|
+
| omitted | json | yes | JSON metadata (no transcript) | (none) |
|
|
1297
|
+
| PATH | any | yes | **error** (incompatible) | -- |
|
|
1298
|
+
| `--list-tenants` | vtt | -- | tenant table | (none) |
|
|
1299
|
+
| `--list-tenants` | json | -- | tenant JSON array | (none) |
|
|
1300
|
+
| `--list-tenants` + PATH | vtt | -- | (empty) | tenant table |
|
|
1301
|
+
| `--list-tenants` + PATH | json | -- | (empty) | tenant JSON |
|
|
1302
|
+
| `--list-transcripts` | table | -- | meeting table | (none) |
|
|
1303
|
+
| `--list-transcripts` | json | -- | meeting JSON array | (none) |
|
|
1304
|
+
| `--list-transcripts` | vtt | -- | **error** (incompatible) | -- |
|
|
1305
|
+
| `--list-transcripts` + PATH | table | -- | (empty) | meeting table |
|
|
1306
|
+
| `--list-transcripts` + PATH | json | -- | (empty) | meeting JSON |
|
|
1307
|
+
|
|
1308
|
+
## Development
|
|
1309
|
+
|
|
1310
|
+
### Setup
|
|
1311
|
+
|
|
1312
|
+
```sh
|
|
1313
|
+
git clone <repo-url> && cd teams-transcripts
|
|
1314
|
+
uv sync --all-groups --locked
|
|
1315
|
+
lefthook install
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
`uv sync` creates the virtual environment in `.venv/` and installs all
|
|
1319
|
+
runtime and development dependencies from the lockfile. Lefthook is an
|
|
1320
|
+
external development prerequisite; install it with `brew install lefthook`
|
|
1321
|
+
on macOS before running `lefthook install` to set up the pre-commit hooks.
|
|
1322
|
+
|
|
1323
|
+
To regenerate the live regression fixtures:
|
|
1324
|
+
|
|
1325
|
+
```sh
|
|
1326
|
+
uv run pytest tests/regression/test_regression.py -m live --update-fixtures
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
See [Generating regression tests](#generating-regression-tests) for details.
|
|
1330
|
+
|
|
1331
|
+
After setup, the wrapper scripts in `bin/` are ready to use:
|
|
1332
|
+
|
|
1333
|
+
```sh
|
|
1334
|
+
./bin/teams-transcripts --help
|
|
1335
|
+
./bin/teams-transcripts-mcp --help
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
These delegate to the venv entry points without requiring `uv run`.
|
|
1339
|
+
|
|
1340
|
+
### Pre-commit hooks
|
|
1341
|
+
|
|
1342
|
+
This project uses [Lefthook](https://github.com/evilmartians/lefthook) for
|
|
1343
|
+
pre-commit checks. The following checks run in parallel on every commit:
|
|
1344
|
+
|
|
1345
|
+
| Check | Tool | Description |
|
|
1346
|
+
|-------|------|-------------|
|
|
1347
|
+
| `ruff-check` | [Ruff](https://docs.astral.sh/ruff/) | Lint with an extended rule set (Pyflakes, pycodestyle, Bugbear, Bandit, and more) |
|
|
1348
|
+
| `ruff-format` | Ruff | Verify formatting matches the canonical style |
|
|
1349
|
+
| `ty-check` | [ty](https://github.com/astral-sh/ty) | Type checking with `--error-on-warning` |
|
|
1350
|
+
|
|
1351
|
+
All three checks must pass before a commit is accepted. Configuration is in
|
|
1352
|
+
`pyproject.toml`.
|
|
1353
|
+
|
|
1354
|
+
### Running checks manually
|
|
1355
|
+
|
|
1356
|
+
```sh
|
|
1357
|
+
uv run ruff check teams_transcripts/ legacy/ tests/ scripts/
|
|
1358
|
+
uv run ruff format --check teams_transcripts/ legacy/ tests/ scripts/
|
|
1359
|
+
uv run ty check --error-on-warning teams_transcripts/ legacy/ tests/ scripts/
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
These checks exclude `tests/regression/`, which is a local, generated test tree
|
|
1363
|
+
containing private meeting configuration and fixtures.
|
|
1364
|
+
|
|
1365
|
+
### Adding a dependency
|
|
1366
|
+
|
|
1367
|
+
```sh
|
|
1368
|
+
uv add <package>
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
This updates both `pyproject.toml` and `uv.lock`.
|
|
1372
|
+
|
|
1373
|
+
### Running tests
|
|
1374
|
+
|
|
1375
|
+
```sh
|
|
1376
|
+
# Safe local suite (live, destructive, and fixture-data tests are deselected)
|
|
1377
|
+
uv run pytest
|
|
1378
|
+
|
|
1379
|
+
# Tracked tests only (for fresh clones without regression/ directory)
|
|
1380
|
+
uv run pytest tests/ --ignore=tests/regression
|
|
1381
|
+
|
|
1382
|
+
# Verbose output with test names
|
|
1383
|
+
uv run pytest -v
|
|
1384
|
+
|
|
1385
|
+
# Specific test file
|
|
1386
|
+
uv run pytest tests/test_domain_models.py
|
|
1387
|
+
|
|
1388
|
+
# Live regression tests (requires Teams running with CDP on port 9333)
|
|
1389
|
+
uv run pytest -m live
|
|
1390
|
+
|
|
1391
|
+
# Override the safe default and run every collected test (may relaunch Teams)
|
|
1392
|
+
uv run pytest -m ""
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
The test suite uses four custom pytest markers. The default marker expression
|
|
1396
|
+
excludes `live`, `destructive`, and `fixtures`; passing `-m` explicitly
|
|
1397
|
+
overrides that default.
|
|
1398
|
+
|
|
1399
|
+
| Marker | Requires | Description |
|
|
1400
|
+
|--------|----------|-------------|
|
|
1401
|
+
| `live` | Running Teams + CDP | Regression tests against real meetings |
|
|
1402
|
+
| `offline` | Generated local configuration | Offline integration accuracy tests |
|
|
1403
|
+
| `destructive` | Running Teams | Tests that kill and relaunch the Teams process |
|
|
1404
|
+
| `fixtures` | Generated fixture files | Offline tests that validate fixture VTT content |
|
|
1405
|
+
|
|
1406
|
+
### Generating regression tests
|
|
1407
|
+
|
|
1408
|
+
> The `tests/regression/fixtures/` directory and local meeting config may contain
|
|
1409
|
+
> real meeting data. Treat regenerated fixtures as sensitive and do not publish
|
|
1410
|
+
> them outside the repository context.
|
|
1411
|
+
|
|
1412
|
+
The regression test suite compares tool output byte-for-byte against reference
|
|
1413
|
+
VTT files in `tests/regression/fixtures/`. These must be generated from a live
|
|
1414
|
+
Teams session:
|
|
1415
|
+
|
|
1416
|
+
```sh
|
|
1417
|
+
uv run pytest tests/regression/test_regression.py -m live --update-fixtures
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
**Requirements:**
|
|
1421
|
+
|
|
1422
|
+
- Microsoft Teams running with CDP enabled on port 9333
|
|
1423
|
+
- Valid GCP credentials for Vertex AI (the LLM agent)
|
|
1424
|
+
- The 9 meetings defined in `tests/regression/test_meetings.yaml` accessible in Teams
|
|
1425
|
+
|
|
1426
|
+
This downloads each meeting's transcript and saves it as
|
|
1427
|
+
`tests/regression/fixtures/regression-{A-I}.vtt`. Once generated, the fixture
|
|
1428
|
+
validation tests run without Teams access.
|
|
1429
|
+
|
|
1430
|
+
To verify fixture-dependent tests pass after generation:
|
|
1431
|
+
|
|
1432
|
+
```sh
|
|
1433
|
+
uv run pytest tests/regression/ -m "not live and not destructive"
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
The regression suite validates the 9 reference meetings in three modes:
|
|
1437
|
+
download (byte-for-byte comparison against fixtures), dry-run (JSON metadata
|
|
1438
|
+
schema), and format-json (output schema validation).
|