はじめに
Model Context Protocol(MCP) は、AIエージェントが外部ツールやデータソースとやり取りするための標準プロトコルとして急速に普及しています。
Zoomも自社のミーティングデータにアクセスするための MCP Server の公開を開始しました。AIクライアントからコネクタとして接続すると、ミーティングの検索やサマリー取得、録画のトランスクリプト取得などが可能になります。
本記事ではまず ChatGPTからZoom MCP Serverに実際に接続 し、何ができるのかを体験します。その上で、裏側で何が起きているのかを理解するために ミニマムなInspectorツールを自作 し、MCP仕様を深掘りしていきます。
対象読者
- MCPの仕様を具体的な実装を通じて理解したい開発者
- Zoom MCP Serverに独自クライアントから接続してみたい方
- OAuth 2.1 + PKCE の実装パターンに興味がある方
Zoom MCP Serverの概要
Zoom MCP Serverの詳細は公式ドキュメントに記載されています。
接続先は2種類のトランスポートが用意されています。
| トランスポート | URL |
|---|---|
| Streamable HTTP | https://mcp.zoom.us/mcp/zoom/streamable |
| SSE(レガシー) | https://mcp.zoom.us/mcp/zoom/sse |
提供されるツールと必要なScope、Rate Limitは以下の通りです。
| ツール | 説明 | 必要なScope | Rate Limit |
|---|---|---|---|
search_meetings |
キーワード・日付範囲でミーティングを検索 | meeting:read:search |
ー |
get_meeting_assets |
サマリー、ノート、録画等のアセット取得 |
meeting:read:assets, meeting:read:assets:admin
|
LIGHT |
get_recording_resource |
トランスクリプト・サマリー・再生URL取得 | cloud_recording:read:content |
MEDIUM |
recordings_list |
ユーザーの録画一覧 | cloud_recording:read:list_user_recordings |
ー |
create_new_file_with_markdown |
Markdownから新規Zoom Docsを作成 | docs:write:import |
HEAVY |
各ツールの詳細なパラメータやレスポンス形式、ScopeやRate Limitは公式ドキュメントを参照してください。
ChatGPTからZoom MCP Serverに接続してみる
まずは実際のAIクライアントからZoom MCP Serverに接続し、何ができるのかを体験してみます。ChatGPTは2025年後半からMCPクライアント機能をサポートしており、カスタムMCPアプリとしてZoom MCP Serverを追加できます。
前提: Zoom Appの作成
Zoom App Marketplace(https://marketplace.zoom.us/)で General App を作成し、Client IDとClient Secretを取得しておきます。
Scopesの設定: Zoom App MarketplaceのApp設定画面で、必要なScopeを追加します。上のツール一覧を参考に、利用したいツールに対応するScopeを追加してください。
1. 開発者モードの有効化
ChatGPTの 設定 → アプリ → 高度な設定 から 開発者モード を有効にします。
これにより、未検証のカスタムMCPコネクタを追加できるようになります。
開発者モードでは、OpenAIによるレビューを受けていないアプリを追加できるようになります。信頼できるMCPサーバーのみを接続してください。
2. アプリの作成
アプリを作成する をクリックし、以下の情報を入力します。
| 項目 | 値 |
|---|---|
| 名前 | 任意 |
| MCPサーバーのURL | https://mcp.zoom.us/mcp/zoom/streamable |
| 認証 | OAuth |
OAuth関連の高度な設定 が右側に展開されます。
クライアントの登録: 「ユーザー定義のOAuthクライアント」を選択し、Zoom App MarketplaceのGeneral Appで作成したClient IDとClient Secretを入力します。
コールバックURL: ChatGPT側が生成するコールバックURLが表示されます。このURLを Zoom AppのOAuth Redirect URL にそのまま設定してください。
トークンエンドポイントの認証方法: client_secret_basic を選択します。
スコープ: 前述のツール一覧に対応するScopeにチェックを入れます。
ChatGPT側の「コールバックURL」とZoom App側の「OAuth Redirect URL」が一致していないとOAuthフローが失敗します。ChatGPTのアプリ作成画面に表示されるURLをコピーして、Zoom App Marketplaceの設定に貼り付けてください。
設定が完了したら「理解したうえで、続行します」にチェックを入れ、作成する をクリックします。
3. OAuth認可
アプリの作成後、初回利用時にZoomのOAuth同意画面が表示されます。
Allow をクリックして認可を完了します。
4. アプリの有効化を確認
認可が完了すると、ChatGPTの 設定 → アプリ に作成したアプリが「有効化されたアプリ」として表示されます。
5. チャットで使ってみる
開発者モードのチャットを開き、作成したアプリを指定して質問します。
例えば「過去3ヶ月に実施された会議についての情報をまとめて」と質問すると、ChatGPTがZoom MCP Server経由で search_meetings ツールを呼び出し、実際のミーティングデータを取得して回答を生成します。
「ツールが呼び出されました」という表示とともに、取得されたミーティング一覧がリッチなUIで表示され、全体サマリまで生成してくれます。
では、この裏側で何が起きているのでしょうか?ここからはMCPの通信仕様を、自作Inspectorで可視化していきます。
Inspectorを自作して仕様を可視化する
ChatGPTがZoom MCP Serverと通信している裏側では、OAuth認証、JSON-RPCハンドシェイク、ツール呼び出しという3段階の通信が行われています。これを可視化するために、ミニマムなInspectorツールをNext.jsで自作しました。
GitHubリポジトリ:
全体アーキテクチャ
Next.jsのAPI Routesが「MCPクライアント兼OAuthリレー」として機能し、ブラウザからは直接MCP Serverにアクセスしません。この構成にしている理由は以下の通りです。
- ブラウザからの直接アクセスではCORSで弾かれる
- OAuth 2.1のトークン交換でClient Secretを使うため、サーバーサイドに置く必要がある
- MCP Session IDの管理をCookie(iron-session)で行う
プロジェクト構成
zoom-mcp-inspector/
├── src/
│ ├── lib/
│ │ ├── session.ts # iron-session 暗号化Cookie
│ │ ├── oauth.ts # PKCE生成、メタデータディスカバリ、トークン交換
│ │ └── mcp-client.ts # JSON-RPC 2.0 クライアント
│ └── app/
│ ├── api/
│ │ ├── discovery/route.ts # Well-knownメタデータ取得
│ │ ├── auth/route.ts # OAuth認可開始
│ │ ├── callback/route.ts # OAuth コールバック
│ │ └── mcp/
│ │ ├── initialize/route.ts # MCP初期化
│ │ ├── tools/route.ts # ツール一覧取得
│ │ └── call/route.ts # ツール実行
│ ├── page.tsx # ランディングページ
│ └── inspector/page.tsx # Inspector UI
├── .env.example
└── package.json
第1段階: OAuth 2.1 + PKCE 認証
MCP仕様における認証の位置づけ
MCP仕様(2025-03-26)では、Streamable HTTP上のMCPサーバーに対する認証として OAuth 2.1 が推奨されています。認可コードフローに加え、PKCE(Proof Key for Code Exchange) が必須とされています。
ディスカバリの流れ
MCP仕様では、未知のMCPサーバーに接続する汎用クライアントを想定して、以下のディスカバリ手順が定義されています。
Step 1の「POSTして401を受け取る」ステップは、接続先のMCPサーバーが認証を要求するかどうか不明な場合の検知手段です。Zoom MCP Serverのように認証が必要なことがわかっている場合は、Step 2のディスカバリから開始するか、Zoomの既知のOAuthエンドポイントに直接アクセスしても問題ありません。本Inspectorでは「MCP仕様上の全フローを体験する」目的で3段階すべてを実装しています。
PKCE の実装
PKCEはOAuth 2.1で必須となったセキュリティ機構で、認可コードの横取り攻撃を防ぎます。
-
code_verifier:crypto.randomBytes(32)→ base64url エンコード(43〜128文字) -
code_challenge:code_verifierを SHA-256 ハッシュ → base64url エンコード - 認可リクエストに
code_challenge+code_challenge_method=S256を付与 - トークン交換時に元の
code_verifierを送信
// oauth.ts — PKCE生成
export function generateCodeVerifier(): string {
return crypto.randomBytes(32)
.toString("base64url");
}
export async function generateCodeChallenge(verifier: string): Promise<string> {
const hash = crypto.createHash("sha256")
.update(verifier)
.digest();
return hash.toString("base64url");
}
メタデータディスカバリの実装
Inspectorでは、well-knownが取得できなかった場合のフォールバックとして、Zoomの既知エンドポイントに直接アクセスする3段階戦略をとっています。
// oauth.ts — メタデータディスカバリ(3段階フォールバック)
export async function discoverAuthMetadata(mcpServerUrl: string) {
const base = new URL(mcpServerUrl);
// Step 1: oauth-protected-resource
try {
const prUrl = `${base.origin}/.well-known/oauth-protected-resource`;
const prRes = await fetch(prUrl);
if (prRes.ok) {
const prData = await prRes.json();
// authorization_servers[0] を使って次のディスカバリへ
}
} catch {}
// Step 2: oauth-authorization-server
try {
const asUrl = `${base.origin}/.well-known/oauth-authorization-server`;
const asRes = await fetch(asUrl);
if (asRes.ok) {
const asData = await asRes.json();
return {
authorizationEndpoint: asData.authorization_endpoint,
tokenEndpoint: asData.token_endpoint,
};
}
} catch {}
// Step 3: Zoomの既知エンドポイントにフォールバック
return {
authorizationEndpoint: "https://zoom.us/oauth/authorize",
tokenEndpoint: "https://zoom.us/oauth/token",
};
}
セッション管理
OAuthフローは複数のHTTPリクエストにまたがるため、状態を保持する仕組みが必要です。ここでは iron-session を使い、暗号化されたCookieに以下を保存しています。
// session.ts
export interface SessionData {
oauthState?: string; // CSRF対策用ランダム文字列
codeVerifier?: string; // PKCEのverifier(トークン交換で使用)
accessToken?: string; // 認証完了後のアクセストークン
refreshToken?: string; // リフレッシュトークン
mcpSessionId?: string; // MCP初期化後のセッションID
authMetadata?: {
authorizationEndpoint: string;
tokenEndpoint: string;
};
}
第2段階: MCP Streamable HTTP
Streamable HTTPとは
MCP仕様 2025-03-26 で導入された Streamable HTTP は、従来のSSEトランスポートを置き換える新しい通信方式です。主な特徴は以下の通りです。
-
単一エンドポイント: 1つのHTTPパスが
GETとPOSTの両方を受け付ける -
セッション管理:
Mcp-Session-Idヘッダーでステートフルなセッションを維持 -
レスポンス形式の柔軟性:
Content-Typeに応じてJSONレスポンスまたはSSEストリームを返す
JSON-RPC 2.0 メッセージフォーマット
MCPの全通信は JSON-RPC 2.0 に準拠しています。リクエストの基本構造は以下の通りです。
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "zoom-mcp-inspector",
"version": "1.0.0"
}
}
}
MCP初期化ハンドシェイク
MCPセッションの確立には2段階のハンドシェイクが必要です。
Step 1: initialize リクエスト — クライアントがプロトコルバージョンと自身の能力を宣言します。サーバーはサポートするプロトコルバージョン・能力・サーバー情報を返します。レスポンスの Mcp-Session-Id ヘッダーを以降のリクエストで使い続けます。
Step 2: notifications/initialized 通知 — クライアントが初期化完了を通知します。これは id フィールドを持たないJSON-RPC通知で、サーバーは 202 Accepted を返します。
notifications/initialized は id を持たない点に注意してください。JSON-RPC 2.0仕様では、id がないメッセージは「通知(notification)」であり、レスポンスを期待しません。id を含めて送信するとプロトコル違反となる可能性があります。
// mcp-client.ts — 初期化フロー
export async function mcpInitialize(
serverUrl: string,
accessToken: string,
existingSessionId?: string
) {
// Step 1: initialize
const initRes = await fetch(serverUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
Authorization: `Bearer ${accessToken}`,
...(existingSessionId && { "Mcp-Session-Id": existingSessionId }),
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "zoom-mcp-inspector", version: "1.0.0" },
},
}),
});
const sessionId = initRes.headers.get("mcp-session-id");
const result = await initRes.json();
// Step 2: notifications/initialized(id なし = 通知)
await fetch(serverUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
...(sessionId && { "Mcp-Session-Id": sessionId }),
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "notifications/initialized",
// id は含めない
}),
});
return { result, sessionId };
}
第3段階: ツール呼び出し
tools/list — 利用可能なツールの取得
MCPサーバーが提供するツールの一覧を取得します。
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
レスポンスには各ツールの名前、説明、入力スキーマ(JSON Schema)が含まれます。ChatGPTの「ツールが呼び出されました」の裏側では、まさにこの tools/list でスキーマを取得し、ユーザーの質問に合ったツールと引数をLLMが選定しています。
tools/call — ツールの実行
取得したツール名と引数を指定してツールを呼び出します。
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "search_meetings",
"arguments": {
"q": "sprint planning",
"from": "2026-04-01T00:00:00Z",
"to": "2026-04-28T00:00:00Z"
}
}
}
実際に取れるデータ
search_meetings のレスポンス(抜粋)
{
"meetings": [
{
"meeting_uuid": "57ABBB84-8847-430D-A963-F9164FC5B32A",
"topic": "Sprint Planning",
"meeting_start_time": "2026-04-27T03:44:33Z",
"host_name": "Michitaka Sugi",
"attendees": [
{ "user_name": "Michitaka Sugi" },
{ "user_name": "Guest" }
],
"has_summary": true,
"has_transcript": true,
"has_recording": true
}
]
}
取得した meeting_uuid を使って get_recording_resource を呼ぶと、AI生成サマリーとタイムスタンプ付きのトランスクリプトが得られます。
get_recording_resource のレスポンス(抜粋)
{
"summaries": [
{
"overall_summary": "Michitaka led a debate on the paradox of modern cognitive optimization...",
"recording_start": 1777261515000
}
],
"transcripts": [
{
"timeline": [
{
"display_name": "Michitaka Sugi",
"text": "Welcome to the debate.",
"ts": "00:00:12.420",
"end_ts": "00:00:14.319"
}
]
}
],
"play_urls": [
{
"urls": ["https://ssrweb.zoom.us/replay02/..."]
}
]
}
トランスクリプトでは 誰が(display_name)・いつ(ts/end_ts)・何を言ったか(text) がタイムスタンプ付きで構造化されています。これにより、「誰がどのトピックについて発言したか」をプログラム的に分析できます。
Inspectorのセットアップ
前提条件
- Node.js
- ngrok(OAuthリダイレクト用)
- Zoom開発者アカウント(前述のGeneral Appを流用可能)
1. セットアップ
git clone https://github.com/YOUR_USER/zoom-mcp-inspector.git
cd zoom-mcp-inspector
npm install
cp .env.example .env.local
.env.local を編集します。
ZOOM_CLIENT_ID=your_client_id
ZOOM_CLIENT_SECRET=your_client_secret
ZOOM_MCP_SERVER_URL=https://mcp.zoom.us/mcp/zoom/streamable
PUBLIC_URL=https://your-ngrok-url.ngrok-free.app
SESSION_SECRET=ランダムな32文字以上の文字列
2. ngrokの起動
ngrok http 3000
表示されたURLを .env.local の PUBLIC_URL と、Zoom AppのOAuth Redirect URLの両方に設定してください。
3. 起動
npm run dev
4. 使い方
-
http://localhost:3000にアクセス - Run Discovery → OAuthメタデータの取得を確認
- Login with Zoom → Zoomの認可画面でアプリを承認
- 認可後、Inspector画面にリダイレクト
- Initialize → MCPセッションの確立
- List Tools → 利用可能なツール一覧の取得
- ツールを選択 → 引数をJSON入力 → Call Tool → レスポンスを確認
アクセストークンの有効期限は約1時間です。期限切れの場合は再度ログインしてください。本ツールは開発・検証目的であり、本番環境での使用は想定していません。
まとめ
本記事ではまずChatGPTからZoom MCP Serverに接続して体験した上で、自作Inspectorで裏側の通信を可視化しました。
- OAuth 2.1 + PKCE: Well-knownメタデータディスカバリ → 認可コード取得 → PKCE付きトークン交換
-
MCP Streamable HTTP: JSON-RPC 2.0 による
initialize→notifications/initializedの2段階ハンドシェイク、Mcp-Session-Idによるセッション管理 -
ツール呼び出し:
tools/listでスキーマ取得 →tools/callで実行 → 構造化されたレスポンスの取得
ChatGPTから「過去の会議をまとめて」と聞いたときに裏側で動いているのは、まさにこのJSON-RPCの通信です。MCPは、AIエージェントが外部データソースと対話するための「標準化されたインターフェース」を提供しています。Zoomのミーティングデータという具体的なデータソースを通じて、その仕組みを体感していただければ幸いです。
参考リンク











