Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Transcripts MCP ServerGet the transcript for my 'Project Kickoff' meeting from earlier today."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Transcripts MCP Server
A remote Model Context Protocol (MCP) server that retrieves Microsoft Teams meeting transcripts via the Microsoft Graph API, using delegated OAuth 2.0 On-Behalf-Of (OBO) authentication.
Hosted on Azure Container Apps and designed for integration with Microsoft Copilot Studio (via the MCP Wizard), though any MCP-compatible client can connect.
Table of Contents
Use Cases
Once an AI agent can access the full text of a meeting, the transcript becomes a launchpad for downstream automation:
Use Case | Description |
Sentiment analysis | Gauge how a customer call actually went — detect frustration, satisfaction, or escalation patterns across every interaction, not just the ones a manager happened to attend. |
Follow-on automation | Extract action items, decisions, and deadlines, then push them into Power Automate flows — create Planner tasks, send follow-up emails, or update CRM records automatically. |
Customer service reviews | Audit support calls at scale without replaying hours of recordings. Search across transcripts for specific topics, complaints, or compliance language. |
Deal intelligence | Surface objections, competitor mentions, pricing commitments, and next steps from sales calls — feed them into your pipeline reporting. |
Training & coaching | Identify coaching moments by analysing how reps handle objections, discovery questions, or product demos. Compare top performers against the team. |
Compliance & audit | Verify that required disclosures, disclaimers, or consent language were delivered during regulated conversations. |
Meeting summaries on demand | Let users ask an agent "What did we decide in the design review?" and get a structured answer — without anyone having to write meeting notes. |
The server returns clean, speaker-attributed text — ready for any LLM to analyse, summarise, or act on.
Features
Three MCP tools: List meetings, retrieve transcripts, and save transcripts to SharePoint for RAG / archival
Delegated-only permissions: The server never has its own access — every Graph call runs in the signed-in user's context via OBO
Calendar-based discovery: Uses
/me/calendarViewto find meetings, then resolves each to an online meeting ID — works around severe/me/onlineMeetingsAPI limitationsOptimised name search: Filters calendar events by subject before resolving to online meetings (avoids unnecessary API calls)
VTT cleaning: Strips all WebVTT metadata (timestamps, cue IDs, NOTE blocks, HTML tags) and merges consecutive same-speaker lines into readable paragraphs
Comprehensive logging: All Graph API calls are traced with
[graph]prefixes for debuggingStateless container: Scales to zero when idle, ~250ms cold start on Alpine Node.js 20
Built for Copilot Studio: Drop-in MCP server with OAuth 2.0 wizard support
Combining with Other MCP Servers
MCP is designed to be composable — a single Copilot Studio agent can connect to multiple MCP servers simultaneously, each providing different tools. This Transcripts MCP server becomes significantly more powerful when paired with other Microsoft 365 MCP servers.
With the Office 365 Outlook / Meeting Management MCP
Copilot Studio includes a built-in Office 365 Outlook MCP connector (Meeting Management MCP Server) that provides tools for listing, creating, and managing calendar events. When both servers are connected to the same agent:
Agent capability | How it works |
"What meetings do I have today?" | The Outlook MCP lists today's calendar events (including non-Teams meetings). |
"Get the transcript from the Design Review" | The Transcripts MCP finds the meeting and returns the cleaned transcript. |
"Summarise my Monday standup and create follow-up tasks" | The agent chains both servers — retrieves the transcript, then uses the Outlook MCP to schedule follow-up meetings or send recap emails. |
The LLM in Copilot Studio automatically decides which MCP server's tools to call based on the user's prompt. No manual routing is needed — the agent sees all available tools from all connected servers and plans accordingly.
Multi-Tenant Agents (Preview)
Copilot Studio now supports multi-tenant agents as a preview feature, allowing you to deploy a single agent across multiple Entra ID tenants. Combined with a remote MCP server like this one (hosted on Azure Container Apps with OBO auth), you can offer transcript-powered AI agents as a managed service to multiple organisations — each user authenticates with their own tenant and only sees their own meetings.
Example: Multi-Server Agent Architecture
Architecture
Auth Flow
Key design decision: All Graph API permissions are delegated — the server only accesses meetings and transcripts the signed-in user has permission to see. There is no application-level access.
How It Works — Internals
This section explains the internal workings of the server for contributors and anyone wanting to understand the design decisions.
End-to-End Pipeline
When a user asks "Get the transcript for the Design Review meeting", the following chain executes:
This is 5 separate Graph API calls per transcript retrieval (calendarView → onlineMeetings → transcripts → content), but by filtering by name before resolving meetings, the server avoids unnecessary API calls for events the user didn't ask about.
Meeting Discovery (Calendar API)
The server uses /me/calendarView instead of /me/onlineMeetings for meeting discovery. This was a deliberate architectural decision driven by severe undocumented limitations in the /me/onlineMeetings endpoint:
What you'd expect to work | What actually happens |
| 400 — endpoint requires |
| 400 — |
| 400 — |
| 400 — |
The only supported filter on /me/onlineMeetings is JoinWebUrl eq '...' — which requires you to already know the join URL.
Solution: Use the Calendar API (/me/calendarView) which supports date ranges natively, include onlineMeeting in $select, then filter client-side for events with a join URL. Resolve each join URL via /me/onlineMeetings?$filter=JoinWebUrl eq '...' to get the meeting ID needed for transcript access.
Meeting Resolution (OnlineMeetings API)
Each calendar event with a Teams join URL must be resolved to an onlineMeeting object. This is handled by resolveOnlineMeeting():
Try exact match:
$filter=JoinWebUrl eq '<joinUrl>'Try decoded URL: Some Graph tenants store the decoded form —
decodeURIComponent(joinUrl)is tried if the exact match failsNon-throwing: Resolution failures are logged (
[graph] GET failed:) rather than silently swallowed, so cross-tenant meetings (403) or network issues are visible in container logs
Cross-tenant meetings: If the user's calendar contains meetings organised in a different Entra ID tenant, the /me/onlineMeetings endpoint returns 403. This is expected — the meeting object belongs to the organiser's tenant. The server logs these and continues to the next event.
Transcript Download & Cleaning
Raw Teams transcripts are in WebVTT format and contain significant metadata:
The cleanVttTranscript() function in vtt-parser.ts:
Strips: WEBVTT header, all timestamp lines (
00:00:00.000 --> ...), cue IDs (numeric and UUID), NOTE blocks,<v>and</v>HTML voice tags (converting toSpeaker: textformat), any remaining HTML tagsMerges: Consecutive lines from the same speaker into single paragraphs
Output:
MCP Transport
The server uses Streamable HTTP transport in stateless mode:
One : A fresh MCP
Serveris created for every incomingPOST /mcp, wired with the user's Graph token, and disposed after the response. No sessions are maintained.sessionIdGenerator: undefined: Disables MCP session management — each request is independent.Why stateless: Container Apps scales to zero when idle. Stateful sessions would break across cold starts and replica restarts. Copilot Studio sends every tool call as an independent HTTP request with its own bearer token, so session state is unnecessary.
Implementation note: The server uses the low-level
Serverclass from@modelcontextprotocol/sdk, not the higher-levelMcpServerclass. This avoids a TypeScriptTS2589(deep type instantiation) error triggered by Zod's optional schemas in the SDK's type inference. The low-level API works identically but requires manualsetRequestHandler()wiring.
Authentication Chain
Security properties:
The server's client secret authenticates the app to Entra ID, but the access is always the user's
If a user doesn't have access to a meeting or transcript, Graph will deny the request
Tokens are never stored — each request does a fresh OBO exchange
No application-level permissions are used
Tools
list_recent_meetings
Lists recent Microsoft Teams online meetings for the signed-in user.
Parameter | Type | Required | Description |
| string | No | Filter meetings to this date (YYYY-MM-DD) |
| number | No | Maximum results to return (default: 10, max: 50) |
Returns: Meeting subject, start/end times, meeting ID, and whether a transcript is available.
get_meeting_transcript
Retrieves and cleans the transcript for a specific Teams meeting.
Parameter | Type | Required | Description |
| string | Yes | Meeting subject to search for (partial match, case-insensitive) |
| string | No | Date filter (YYYY-MM-DD) to narrow results |
Returns: Clean speaker-attributed text with all VTT metadata stripped. The output is ready for AI summarisation, action item extraction, or semantic search.
save_transcript
Retrieves a meeting transcript and saves it to a SharePoint document library as a Markdown file. The file includes speaker attribution and is formatted for RAG indexing (e.g. by Microsoft 365 Copilot or Azure AI Search). Also returns the transcript text in the response for immediate use.
Parameter | Type | Required | Description |
| string | Yes | Meeting subject to search for (partial match, case-insensitive) |
| string | No | Date filter (YYYY-MM-DD) to narrow results |
| string | No | SharePoint site URL (e.g. |
| string | No | Folder path in the document library (e.g. |
Returns: The cleaned transcript text plus a confirmation with the SharePoint web URL of the uploaded file.
File naming: {Subject}_{YYYY-MM-DD}.md — e.g. Design_Review_2026-02-18.md
RAG integration: Files saved to SharePoint are automatically indexed by Microsoft 365 Copilot (no extra setup). For custom RAG, use the Azure AI Search SharePoint indexer to pull content into your own search index.
Prerequisites
Azure Subscription with Container Apps support
Azure Container Registry (Basic SKU is sufficient)
Microsoft Entra ID — ability to create App Registrations and grant admin consent
Node.js ≥ 20 (for local development only)
Docker (optional — ACR can build images remotely via
az acr build)Azure CLI (
az) installed and logged inA Microsoft 365 licence with Teams meetings and transcription enabled
Quick Start
For production deployment to Azure, see Deploy to Azure Container Apps.
Azure App Registration Setup
1. Register the Application
Click New registration
Name:
Transcripts MCP ServerSupported account types: Single tenant (your org only)
Redirect URI: Leave blank (added in step 5)
Note the Application (client) ID and Directory (tenant) ID
2. Create a Client Secret
Go to Certificates & secrets → New client secret
Description:
mcp-server-secret, Expiry: 24 monthsCopy the secret value immediately — it won't be shown again
3. Expose an API (Required for OBO)
This is the critical step that enables the On-Behalf-Of flow. Without it, the OBO token exchange will fail.
Go to Expose an API
Click Set next to "Application ID URI" → accept the default
api://<client-id>Click Add a scope:
Scope name:
access_as_userWho can consent: Admins and users
Admin consent display name:
Access Transcripts MCP as userAdmin consent description:
Allows the app to access meeting transcripts on behalf of the signed-in userUser consent display name:
Access your meeting transcriptsUser consent description:
Allows this app to read your Teams meeting transcriptsState: Enabled
The full scope URI will be:
api://<client-id>/access_as_user
4. Configure API Permissions
Go to API permissions → Add a permission → Microsoft Graph → Delegated permissions
Add these four permissions:
Permission
Purpose
User.ReadSign in and read user profile; enables
/meendpointsCalendars.ReadRead calendar events via
/me/calendarViewto discover Teams meetingsOnlineMeetings.ReadLook up online meeting details via
/me/onlineMeetings?$filter=JoinWebUrl eq '...'OnlineMeetingTranscript.Read.AllRead transcript metadata and content
Sites.ReadWrite.AllUpload transcript files to SharePoint (for
save_transcripttool)Click Grant admin consent for [your tenant]
Important: After granting admin consent, verify the consent grant includes all five scopes. If the grant was created before all permissions were added, you may need to update it. See Troubleshooting → Verifying Admin Consent Grants.
5. Configure Authentication (Redirect URIs)
Go to Authentication → Add a platform → Web
Add the following redirect URIs:
URI
Purpose
https://token.botframework.com/.auth/web/redirectBot Framework / Power Platform auth
https://copilotstudio.microsoft.com/auth/callbackCopilot Studio web callback
https://global.consent.azure-apim.net/redirect/<your-connector-id>Copilot Studio MCP connector (provided in the MCP wizard)
Click Save
Note: The third URI is specific to your Copilot Studio connector. When you set up the MCP connection in Copilot Studio, it will display the exact redirect URI you need to register. You must add it or you will get
AADSTS500113.
6. Authorise Client Applications (Optional)
If Copilot Studio provides a client application ID:
Go to Expose an API → Authorised client applications
Add the Copilot Studio client application ID
Check the
access_as_userscope
Environment Variables
Variable | Required | Default | Description |
| Yes | — | Application (client) ID from App Registration |
| Yes | — | Client secret value from App Registration |
| Yes | — | Directory (tenant) ID |
| No |
| HTTP server port |
| No | — | Default SharePoint site for |
| No |
| Default folder path in the document library |
Local Development
The server will start on http://localhost:8080 (or the port specified in .env).
Test Endpoints
Docker Build & Run
The Dockerfile uses a multi-stage build (node:20-alpine) with a non-root user for security:
Builder stage: Installs all dependencies, compiles TypeScript
Production stage: Copies only compiled JS + production dependencies, runs as
mcpuser(non-root)
Deploy to Azure Container Apps
Deploy from Scratch
Redeploy After Code Changes
View Container Logs
All Graph API operations are logged with [graph] prefixes, making it straightforward to trace the pipeline:
Update Environment Variables
Copilot Studio Integration
MCP Wizard Configuration
In Copilot Studio, open your agent → Tools → Add a tool
Select MCP Server
Enter the MCP server URL:
URL:
https://<your-app-fqdn>/mcp
Select Authentication: OAuth 2.0
Fill in the OAuth 2.0 settings:
Field
Value
Client ID
Your Application (client) ID
Client Secret
Your client secret value
Authorization URL
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorizeToken URL
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/tokenRefresh URL
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token(same as Token URL)Scope
api://<client-id>/access_as_userCopy the redirect URI shown by the wizard (e.g.,
https://global.consent.azure-apim.net/redirect/...)Register the redirect URI in the App Registration (see Step 5)
Test the connection — you should see both tools discovered:
list_recent_meetingsget_meeting_transcript
Example Prompts
Once connected, users can ask the Copilot:
"What meetings do I have today?"
"Show me my recent meetings"
"Get the transcript from the Design Review meeting"
"What did Sarah say in yesterday's standup?"
"Summarise the TredStone meeting from Tuesday"
Example Output
User: "Get the transcript for the TredStone meeting"
Copilot Studio calls get_meeting_transcript(meetingName="TredStone"), which returns clean speaker-attributed text with meeting metadata:
The agent then analyses the transcript and presents structured insights to the user.
Using the Meeting Link for Downstream Actions
Every transcript response includes a Meeting link — the Teams join URL for that meeting. This link points back to the original Teams meeting where the full recording, attendance report, and rich transcript (with timestamps and speaker timeline) are stored.
This is important for the use cases described earlier:
Use Case | How the Meeting Link Helps |
Compliance & audit | The link provides a verifiable reference back to the original meeting record. Auditors can click through to Teams to access the full recording and attendance list. |
Customer service reviews | Managers can share the meeting link with coaches or reviewers so they can listen to specific sections of the recording alongside the transcript. |
Training & coaching | The link allows trainers to jump directly into the Teams meeting to replay key moments — the AI-cleaned transcript identifies what was said, the recording shows how it was said. |
Deal intelligence | Sales leaders can follow the link to review the full meeting context when the transcript flags an objection or competitor mention. |
Follow-on automation | Power Automate flows can use the meeting link as a reference URL when creating Planner tasks or CRM entries from action items. |
The agent can also hand the meeting link to other MCP tools (e.g., the Office 365 Outlook MCP) to schedule follow-up meetings that reference the original discussion.
Agent Instructions
To get the best results from a Copilot Studio agent connected to this MCP server, use structured agent instructions that tell the agent how to find information (via MCP tools) rather than relying on knowledge or memory.
Below is a recommended set of agent instructions. Paste this into your Copilot Studio agent's Instructions field:
You are a digital employee named "Meeting Agent" who serves as a persistent organizational actor for client meetings.
Your Role
You can be invited to client meetings and your core responsibility is to:
Retrieve meeting transcripts from past meetings
Capture rich, structured notes from those transcripts
Take autonomous action after explicit confirmation
How You Find Information
You MUST use MCP tools to find meetings and transcripts. Never attempt to answer from knowledge or memory.
Step 1 — Find the meeting Use the Meeting Management MCP (Office 365 Outlook) tool
Step 2 — Get the transcript Use the Transcripts MCP Server tools:
list_recent_meetingsget_meeting_transcript
Step 3 — Analyse and respond Once you have the transcript text, analyse it to answer the user's question or produce the structured output described below.
If the user asks about a meeting and you cannot find it via the MCP tools, tell them — do not fabricate or guess content. If a transcript is not available, explain that transcription may not have been enabled for that meeting.
Handling Meeting Metadata
When the transcript response includes a header section (before the
Meeting link
Date
Transcript ID
Only show the full transcript when the user explicitly asks for it. For questions about meeting details, extract the relevant metadata from the header.
What You Capture
When analysing a transcript, extract the following structured information:
Sentiment
Timeline
Products/Funds Mentioned
Sentiment per Product
Action Items
Decisions Made
Risks & Concerns
Next Best Actions
Saving Transcripts
If the user asks to save or archive a transcript to SharePoint, use the
Response Format
When summarising a meeting, use this structure:
Rules
Always retrieve real data via MCP tools — never guess or use training knowledge for meeting content
If multiple meetings match a search, present the options and ask the user to clarify
Keep summaries factual — attribute statements to speakers where possible
Ask for confirmation before taking any action (sending emails, creating tasks, saving files)
Never show raw JSON — always format responses for readability
Tip: These instructions work with the Meeting Management MCP (Office 365 Outlook) and Transcripts MCP Server connected as tools on the same agent. The agent uses the Outlook MCP for calendar discovery and this server for transcript retrieval — the LLM automatically routes to the right tool based on what the user asks.
Copilot Studio then automatically summarises the raw transcript into structured insights, action items, and highlights for the user.
API Reference
GET /health
Returns server health status. No authentication required.
Response (200):
POST /mcp
MCP protocol endpoint. Requires Authorization: Bearer <token> header.
Request headers:
Content-Type: application/jsonAccept: application/json, text/event-streamAuthorization: Bearer <user-access-token>
Supported MCP methods:
Method | Description |
| MCP protocol handshake. Returns server capabilities and protocol version. |
| Returns the list of available tools with their input schemas. |
| Executes a tool and returns results. |
Error responses:
Status | Meaning |
401 | Missing or invalid Authorization header |
403 | OBO token exchange failed (bad credentials or consent) |
405 | Wrong HTTP method (GET or DELETE to /mcp) |
500 | Internal server error |
GET /mcp | DELETE /mcp
Returns 405 Method Not Allowed. The MCP transport is Streamable HTTP (POST only, stateless).
Project Structure
Module Details
Module | Lines | Purpose | Key Exports |
| ~300 | Express HTTP server + MCP protocol wiring. Creates a new | Express app, |
| ~80 | MSAL OBO token exchange. Creates |
|
| ~290 | Microsoft Graph REST client. Uses Calendar API for meeting discovery, resolves join URLs to online meeting IDs, fetches transcripts. Includes |
|
| ~130 | Strips VTT metadata (headers, timestamps, cue IDs, NOTEs, HTML tags). Converts |
|
Dependencies
Package | Version | Purpose |
| ^1.12.1 | MCP server and Streamable HTTP transport |
| ^2.16.2 | MSAL ConfidentialClientApplication for OBO flow |
| ^4.21.2 | HTTP framework |
| ^3.24.2 | Schema validation (MCP SDK dependency) |
Permissions Deep Dive
Delegated Permissions (Microsoft Graph)
Permission | Type | API Endpoint | Why Needed |
| Delegated |
| Required for sign-in; enables all |
| Delegated |
| Discover Teams meetings from the user's calendar |
| Delegated |
| Resolve calendar events to online meeting IDs |
| Delegated |
| List and download transcript content (VTT) |
| Delegated |
| Upload transcript files to SharePoint document libraries |
Custom Scope
Scope | URI | Purpose |
|
| Exposed by the App Registration to enable the OBO flow. Copilot Studio requests this scope when authenticating the user. |
Security note: The server never accesses meetings with its own application identity. Every Graph API call uses a delegated token obtained via OBO, meaning it runs in the context of the signed-in user. If the user doesn't have access to a meeting or transcript, the Graph API will deny the request.
Teams Admin Requirements
For transcripts to be available, the following must be true:
Transcription must be enabled in the Teams admin centre (or via policy)
A meeting organiser or participant must start transcription during the meeting
The signed-in user must be an organiser or participant of the meeting
Troubleshooting
Authentication Errors
Error | Cause | Solution |
| No bearer token in request | Ensure Copilot Studio is configured with OAuth 2.0 and sends the |
| OBO token exchange failed | Check |
| Missing redirect URI | Add the redirect URI from the Copilot Studio MCP wizard to Authentication → Web → Redirect URIs. |
| Admin consent not granted/incomplete | Click Grant admin consent in API permissions. Verify the grant includes all five scopes (see below). Common pitfall: If you added permissions after the initial consent grant, the grant is NOT automatically updated — you must re-grant or patch it. |
| Wrong client secret or tenant | Regenerate the client secret and update the env var. |
| Redirect URI mismatch | Check for trailing slashes and case sensitivity. |
Token Expiry / Session Errors
Error | Cause | Solution |
| Copilot Studio cached an expired token | Fully refresh the browser, or disconnect and reconnect the MCP connection. Starting a "New conversation" alone is not sufficient. |
| Calendar event is for a meeting in a different Entra ID tenant | Expected behaviour. Server logs these as |
Graph API Errors
Error | Cause | Solution |
| No Teams meetings in calendar within date range | Try without a date filter (shows last 30 days + 7 days forward). User must be organiser or invitee. |
| No transcription was started during the meeting | Transcription must be started during the meeting by a participant. Check the transcription policy. |
| Insufficient permissions | Verify all five scopes are in the admin consent grant (see Verifying Admin Consent Grants). |
| Meeting or transcript ID invalid | Meeting may have been deleted. Try |
Graph API Gotchas Discovered During Development:
The
/me/onlineMeetingsendpoint has severe limitations not obvious from the documentation:
Cannot list all meetings — requires
$filter, only supportsJoinWebUrl,joinMeetingIdSettings/joinMeetingId, orVideoTeleconferenceIdNo — rejected with
400
isOnlineMeeting— on/me/calendarView, you must includeonlineMeetingin$selectand filter client-sideDateTimeOffset values must be unquoted — single quotes cause
400 BadRequest
Container / Deployment Errors
Error | Cause | Solution |
Health endpoint times out | Container cold-starting (min replicas = 0) | Wait ~10 seconds and retry. Set |
Container fails to start | Missing env vars or build error | Check logs: |
| Port conflict | Ensure |
Image pull fails | ACR credentials expired |
|
Copilot Studio Errors
Error | Cause | Solution |
"Unable to connect to MCP server" | Wrong URL or server down | Verify URL ends in |
Tools not discovered | MCP handshake fails | Verify |
"Authentication failed" after signing in | OAuth misconfiguration | Check all OAuth fields. The Refresh URL is the same as the Token URL for Entra ID v2.0. |
Verifying Admin Consent Grants
If you suspect the admin consent grant is incomplete:
Expected output: User.Read Calendars.Read OnlineMeetings.Read OnlineMeetingTranscript.Read.All Sites.ReadWrite.All
To fix a grant with missing scopes:
Why does this happen? When you click "Grant admin consent" in the Azure Portal, it creates or updates an
oauth2PermissionGrantobject. However, if permissions were added to the App Registration after the initial grant was created, the portal may not update the existing grant to include the new scopes. The OBO flow then fails withAADSTS65001because the grant doesn't cover all the scopes the server is requesting. The fix is to explicitly re-grant with all scopes using the CLI commands above.
Development History
This project went through 9 iterations to arrive at a working architecture, primarily due to undocumented limitations in the Microsoft Graph /me/onlineMeetings API.
Version | Changes |
v1–v2 | Initial implementation using |
v3 | Removed all OData query options. Failed — endpoint requires |
v4 | Added |
v5 | Removed single-quotes around |
v6 | Architecture change: Switched to Calendar API ( |
v7 | First working version: Removed |
v8 | Extended date range to "30 days back → 7 days forward" to include upcoming meetings. |
v9 | Current version: Comprehensive |
License
MIT