
Zoom Meeting Admin
- 1 installs
- Updated June 10, 2026
- mebusw/zoom-meeting-admin
Manages Zoom meetings, cloud recordings, and account users through a Server-to-Server OAuth Python script restricted to a fixed whitelist of actions.
About
Wraps a Zoom Server-to-Server OAuth REST script to list, view, create, or delete meetings, look up users, and query cloud recordings via a fixed action whitelist. A developer uses it to administer Zoom meetings and recordings from the CLI with credentials in a local .env.
- Whitelisted actions only; create/delete meeting require explicit confirmation
- Server-to-Server OAuth with cached chmod-600 token, no user interaction
Zoom Meeting Admin by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,980 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mebusw/zoom-meeting-admin --skill zoom-meeting-adminAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | June 10, 2026 |
| Repository | mebusw/zoom-meeting-admin ↗ |
What it does
Manages Zoom meetings, cloud recordings, and account users through a Server-to-Server OAuth Python script restricted to a fixed whitelist of actions.
Files
Zoom Server-to-Server OAuth REST API
权限与约束
本 Skill 通过 scripts/zoom-s2s.py 调用 Zoom Server-to-Server OAuth REST API,不实现"通用 REST 代理"。
- 声明的工具:
Bash(python3:*)(执行scripts/zoom-s2s.py)、Bash(ls:*)/Bash(cat:*)(查看脚本输出与缓存)、Read(读取凭证文件与文档)。 - 网络访问:向
https://zoom.us/oauth/token与https://api.zoom.us/v2/*发起 HTTPS 请求,传输头包含Authorization: Bearer <token>。 - 文件写入:在
~/.zoom-s2s-token.json缓存访问令牌(已自动chmod 600)。 - 凭证读取:从仓库根目录的
.env读取ZOOM_ACCOUNT_ID/ZOOM_CLIENT_ID/ZOOM_CLIENT_SECRET/ZOOM_USER_ID。 - 允许的 Action(白名单)——禁止构造任意 Zoom REST 请求或调用未列出的端点:
- 会议:
list_meetings/get_meeting/create_meeting/delete_meeting - 用户:
get_user/list_users - 录像:
recordings - 越权防护:脚本未导出
api_call给上层调用;不得通过修改脚本、注入参数、拼接 URL 等方式旁路调用白名单外的 Zoom 端点(如DELETE /users/{id}、PATCH /accounts/{id}等高风险端点)。 - 强人类确认:
create_meeting与delete_meeting在执行前必须获得用户显式确认;delete_meeting命令还需附加--yes参数。
凭证配置
在 .env 文件中配置(仅 chmod 600,不要提交到任何 Git 仓库):
ZOOM_ACCOUNT_ID=你的AccountID
ZOOM_CLIENT_ID=你的ClientID
ZOOM_CLIENT_SECRET=你的ClientSecret
ZOOM_USER_ID=你的用户邮箱或user_id⚠️ 完整的安全规范见下一节 ## 凭证安全。Token 获取方式:Server-to-Server OAuth,机器对机器,无需用户交互授权。
凭证安全
.env 中的 ZOOM_CLIENT_SECRET 是长期有效的账户级凭据,等同于账户管理员口令。必须遵守:
- 加入 `.gitignore`:本仓库
.gitignore已包含.env;同步确保 IDE、备份工具、文件同步(iCloud / Dropbox / OneDrive / 坚果云)不会自动上传该文件。 - 限制文件权限:
chmod 600 .env;scripts/zoom-s2s.py缓存的~/.zoom-s2s-token.json同样敏感(已自动chmod 600),不要复制到剪贴板、聊天窗口、终端截图、报错工单、AI 对话上下文或第三方日志服务。 - 不要在共享环境复用:CI runner、公用跳板机、容器镜像、共享开发机中复用同一份凭据 ≈ 凭据公开。
Account ID + Client ID + Client Secret三元组可换得 1 小时有效的访问令牌。 - 最小权限:按
## 最小权限配置建议表按需开启 Scope;不需要的 Action 不要勾选对应权限;delete_meeting之外的写权限(meeting:write:update、user:write:*、account:write:*)默认不要开。 - 独立 App:为此 Skill 单独创建一个 Zoom Server-to-Server App,不要复用其他业务 App 的凭据;一旦泄露,旋转该 App 的凭据即可,不影响其他业务。
- 凭据泄露应急:在 Zoom Marketplace 删除该 App → 重新创建并轮换
ACCOUNT_ID/CLIENT_ID/CLIENT_SECRET/USER_ID四项 →rm -f ~/.zoom-s2s-token.json强制下次重新认证 → 复盘泄露路径。
核心脚本
scripts/zoom-s2s.py — 纯 Python,无外部依赖,兼容 Python 3.7+。
cd ~/.agents/skills/zoom-meeting-admin/scripts
# 获取帮助
python3 zoom-s2s.py help
# 列出即将到来的会议
python3 zoom-s2s.py list_meetings <user> <page_size> upcoming
# 获取单个会议详情
python3 zoom-s2s.py get_meeting <meeting_id>
# 创建会议 (start_time: YYYY-MM-DDTHH:MM:SS)
python3 zoom-s2s.py create_meeting "<主题>" "<start_time>" <时长分钟> [时区] [密码]
python3 zoom-s2s.py create_meeting "煎饼果子讨论会" "2026-05-05T10:00:00" 60 Asia/Shanghai
# 删除会议
python3 zoom-s2s.py delete_meeting <meeting_id>
# 获取云录像
python3 zoom-s2s.py recordings <user> <page_size>
# 获取用户信息
python3 zoom-s2s.py get_user [user]
# 列出账户下所有用户
python3 zoom-s2s.py list_users [page_size]Token 缓存
脚本自动缓存 Token 到 ~/.zoom-s2s-token.json(有效期约 50 分钟),重复调用无需每次重新认证。
常用操作快速参考
| 操作 | 命令 |
|---|---|
| 列出最近5个会议 | list_meetings <user> 5 upcoming |
| 列出最近10个历史会议 | list_meetings <user> 10 past |
| 创建明天10点会议 | create_meeting "主题" "YYYY-MM-DDT10:00:00" 60 Asia/Shanghai |
| 获取会议详情 | get_meeting <id> |
| 删除会议 | delete_meeting <id> --yes |
| 获取云录像 | recordings <user> 10 |
最小权限配置建议
根据实际使用场景按需开通 scope,不需要的功能不要授权:
| 功能 | 所需 Scope | 建议 |
|---|---|---|
| 列出会议 | meeting:read:list_meetings | ✅ 核心 |
| 查看会议详情 | meeting:read:meeting | ✅ 核心 |
| 创建会议 | meeting:write:create | 按需开启 |
| 删除会议 | meeting:write:delete | ⚠️ 谨慎开启 |
| 读取云录像 | cloud_recording:read:list_user_recordings | ⚠️ 谨慎开启 |
| 列出账户用户 | user:read:list_users | ⚠️ 谨慎开启 |
建议为此 Skill 单独创建一个 Zoom Server-to-Server App,不要复用已有 App 的凭证。
Agent 调用规范
- 创建会议前:向用户确认主题、时间、时长,再执行。
- 删除会议前:必须向用户明确展示会议信息并获得确认,命令需附加
--yes参数。 - 禁止超范围调用:仅允许文档中列出的 Action,不得构造任意 Zoom REST API 请求。
创建周期性会议
创建 type=8(周期性会议)的 recurrence 参数说明:
| recurrence.type | 说明 | 可用字段 | 是否可用 |
|---|---|---|---|
| 1 | 每日循环(Daily) | end_date_time 或 count | ✅ |
| 2 | 每周循环(Weekly) | weekly_days(字符串), end_date_time 或 count | ✅ |
| 3 | 每月循环(Monthly) | monthly_day 或 monthly_weeks + weekly_days | ✅ |
⚠️ 关键避坑:`weekly_days` 必须是字符串,不是数组!
| 错误写法 | 正确写法 |
|---|---|
"weekly_days": [6] | "weekly_days": "6" |
"weekly_days": ["6"] | "weekly_days": "6"(单日) |
"weekly_days": "6,0"(多日,周六+周日) |
weekly_days 取值:1=周一 ~ 7=周日
示例:创建 5月23日-24日(周六日)的周期性会议:
payload = {
"topic": "CSM公开课",
"type": 8,
"start_time": "2026-05-23T08:00:00",
"duration": 540,
"timezone": "Asia/Shanghai",
"recurrence": {
"type": 2,
"repeat_interval": 1,
"weekly_days": "6,0", # 周六+周日,字符串!
"end_date_time": "2026-05-24T00:00:00Z"
},
"settings": {
"host_video": True,
"participant_video": True,
"join_before_host": False,
"mute_upon_entry": False
}
}多日示例(周一+周三+周五):
"weekly_days": "1,3,5"踩坑记录
1. scope 错误 (4711):某些 API(如 get_user)需要在 App 里开通对应 scope,又如 list_meetings 需要在 App 里开通 meeting:read:list_meetings 权限 2. Token 有效期:Server-to-Server Token 有效期 1 小时,脚本自动刷新并缓存 3. 用户 ID:可用邮箱,也可用 list_users 查 user_id 4. `weekly_days` 必须为字符串:Zoom API 要求 weekly_days 是 "6" 这样的字符串,而非 [6] 数组,传数组会报 300 错误
# =============================================================================
# Zoom Server-to-Server OAuth 凭证配置
# =============================================================================
# 路径: ~/.agents/skills/zoom-s2s-oauth/.env
#
# 获取方式 (Zoom Marketplace):
# 1. https://marketplace.zoom.us/develop/create
# 2. 选择 "Server-to-Server OAuth" → 创建 App
# 3. 填写 Account ID / Client ID / Client Secret
# 4. 在 App 配置里开通需要的 Scope
# =============================================================================
# Account ID (可在 Zoom App 配置页找到)
ZOOM_ACCOUNT_ID=
# Client ID
ZOOM_CLIENT_ID=
# Client Secret
ZOOM_CLIENT_SECRET=
# 默认用户 (邮箱 或 user_id)
ZOOM_USER_ID=
.env
.DS_StoreSKILL of Zoom Server-to-Server OAuth REST API
Manage and schedule Zoom meetings REST API directly via Server-to-Server OAuth — no VPS required, no MCP protocol needed.
EN | 中文
Setup Steps
1. Log in to https://marketplace.zoom.us/ 2. From the "Develop" dropdown, select "build app" and create an app of type Server-to-Server OAuth 3. Add required OAuth Scopes 4. Get the credentials 5. Activate the app 6. Invoke this SKILL in your AI agent
Credentials
Edit the .env file with your Zoom Server-to-Server OAuth App credentials:
ZOOM_ACCOUNT_ID=yourAccountID
ZOOM_CLIENT_ID=yourClientID
ZOOM_CLIENT_SECRET=yourClientSecret
ZOOM_USER_ID=your_user_email_or_user_idRequired OAuth Scopes
Enable these in your Zoom Marketplace Server-to-Server OAuth App:
| Scope | Purpose |
|---|---|
meeting:read:list_meetings | List meetings |
meeting:write:create | Create meetings |
meeting:write:delete | Delete meetings |
cloud_recording:read:list_user_recordings | View cloud recordings |
user:read:list_users | List users |
user:read:user | Get user info |
Quick Start
cd ~/.agents/skills/zoom-s2s-oauth/scripts
# List upcoming meetings
python3 zoom-s2s.py list_meetings service@uperform.cn 5 upcoming
# Create a meeting (start_time format: YYYY-MM-DDTHH:MM:SS)
python3 zoom-s2s.py create_meeting "Pancake Discussion" "2026-05-05T10:00:00" 60 Asia/Shanghai
# Get cloud recordings
python3 zoom-s2s.py recordings service@uperform.cn 10Directory Structure
zoom-s2s-oauth/
├── SKILL.md # AI Agent invocation guide
├── README.md # This file
├── README.zh-cn.md # 中文版
├── .env # Credentials (do NOT commit to git!)
└── scripts/
└── zoom-s2s.py # Main script (pure Python3, no external dependencies)Comparison: MCP vs Server-to-Server REST
| MCP | Server-to-Server REST | |
|---|---|---|
| Requires VPS | ✅ Yes (OAuth callback) | ❌ No |
| Protocol | MCP (JSON-RPC) | Standard REST (pure Python, no deps) |
| Token | User-Managed OAuth | Server-to-Server OAuth |
| Complexity | High (proxy+OAuth) | Low (direct call) |
| Features | Zoom MCP tools | All Zoom REST API |
If you only need core Zoom meeting/recording features, Server-to-Server REST is simpler.
SKILL of Zoom Server-to-Server OAuth REST API
用 Server-to-Server OAuth 直接调 Zoom meeting REST API,不需要 VPS,不需要 MCP 协议,管理和安排ZOOM会议。
EN | 中文
配置步骤
1. 需要先手动登录 https://marketplace.zoom.us/ 2. 在 "Develop" 下拉菜单选 "build app", 创建一个 Server-to-Server OAuth类型的APP 3. 添加必要的OAuth Scope。 4. 获取相关凭证密码 5. 激活APP 6. 在AI agent中调用此 SKILL
凭证配置
编辑 .env 文件,填入你的 Zoom Server-to-Server OAuth App 凭证:
ZOOM_ACCOUNT_ID=你的AccountID
ZOOM_CLIENT_ID=你的ClientID
ZOOM_CLIENT_SECRET=你的ClientSecret
ZOOM_USER_ID=你的用户邮箱或user_id需要的 OAuth Scope
在 Zoom Marketplace 你的 Server-to-Server OAuth App 里开通:
| Scope | 用途 |
|---|---|
meeting:read:list_meetings | 列出会议 |
meeting:write:create | 创建会议 |
meeting:write:delete | 删除会议 |
cloud_recording:read:list_user_recordings | 查看云录像 |
user:read:list_users | 列出用户 |
user:read:user | 获取用户信息 |
快速开始
cd ~/.agents/skills/zoom-s2s-oauth/scripts
# 列出最近5个会议
python3 zoom-s2s.py list_meetings service@uperform.cn 5 upcoming
# 创建会议 (start_time 格式: YYYY-MM-DDTHH:MM:SS)
python3 zoom-s2s.py create_meeting "煎饼果子讨论会" "2026-05-05T10:00:00" 60 Asia/Shanghai
# 获取云录像
python3 zoom-s2s.py recordings service@uperform.cn 10目录结构
zoom-s2s-oauth/
├── SKILL.md # AI Agent 调用说明
├── README.md # 本文件
├── .env # 凭证配置 (不要提交到 git!)
└── scripts/
└── zoom-s2s.py # 主脚本 (纯 Python3,无外部依赖)对比:MCP vs Server-to-Server REST
| MCP 方式 | Server-to-Server REST | |
|---|---|---|
| 需要 VPS | ✅ 需要 (OAuth 回调) | ❌ 不需要 |
| 协议 | MCP (JSON-RPC) | 标准 REST (Python 无外部依赖) |
| Token | User-Managed OAuth | Server-to-Server OAuth |
| 复杂度 | 高 (代理+OAuth) | 低 (直接调) |
| 功能 | Zoom MCP 工具集 | 所有 Zoom REST API |
如果你只需要调用 Zoom 会议/录像等核心功能,Server-to-Server REST 方式更简单。
#!/usr/bin/env python3
"""
Zoom Server-to-Server OAuth REST API 调用脚本 (纯 Python,无外部依赖)
"""
import json
import os
import sys
import base64
import time
import urllib.request
import urllib.parse
import urllib.error
import argparse
from datetime import datetime, timezone
# =============================================================================
# 配置
# =============================================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ENV_FILE = os.path.join(SCRIPT_DIR, "..", ".env")
TOKEN_CACHE = os.path.expanduser("~/.zoom-s2s-token.json")
ZOOM_ACCOUNT_ID = ""
ZOOM_CLIENT_ID = ""
ZOOM_CLIENT_SECRET = ""
ZOOM_USER_ID = ""
# =============================================================================
# .env 加载
# =============================================================================
def load_env():
global ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET, ZOOM_USER_ID
if not os.path.exists(ENV_FILE):
print(f"❌ .env 文件不存在: {ENV_FILE}", file=sys.stderr)
sys.exit(1)
with open(ENV_FILE) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, val = line.split("=", 1)
val = val.strip().strip('"').strip("'")
if key == "ZOOM_ACCOUNT_ID":
ZOOM_ACCOUNT_ID = val
elif key == "ZOOM_CLIENT_ID":
ZOOM_CLIENT_ID = val
elif key == "ZOOM_CLIENT_SECRET":
ZOOM_CLIENT_SECRET = val
elif key == "ZOOM_USER_ID":
ZOOM_USER_ID = val
# =============================================================================
# Token 管理
# =============================================================================
def get_token():
cached_token = ""
cached_expiry = 0
if os.path.exists(TOKEN_CACHE):
try:
with open(TOKEN_CACHE) as f:
d = json.load(f)
cached_token = d.get("access_token", "")
cached_expiry = d.get("expiry", 0)
except Exception:
pass
# 检查缓存是否有效(提前5分钟过期)
if cached_token and cached_expiry > (time.time() + 300):
return cached_token
# 获取新 Token
creds = base64.b64encode(f"{ZOOM_CLIENT_ID}:{ZOOM_CLIENT_SECRET}".encode()).decode()
url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={ZOOM_ACCOUNT_ID}"
req = urllib.request.Request(url, data=b"", method="POST")
req.add_header("Authorization", f"Basic {creds}")
req.add_header("Content-Type", "application/x-www-form-urlencoded")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
result = json.loads(resp.read())
except urllib.error.HTTPError as e:
err = e.read().decode("utf-8", errors="replace")
print(f"❌ Token 获取失败 ({e.code}): {err}", file=sys.stderr)
sys.exit(1)
access_token = result.get("access_token", "")
expires_in = result.get("expires_in", 3600)
if not access_token:
print("❌ Token 响应中无 access_token", file=sys.stderr)
sys.exit(1)
# 缓存,并收紧文件权限(仅当前用户可读写)
with open(TOKEN_CACHE, "w") as f:
json.dump({"access_token": access_token, "expiry": time.time() + expires_in - 300}, f)
os.chmod(TOKEN_CACHE, 0o600)
return access_token
# =============================================================================
# API 调用
# =============================================================================
def api_call(method, path, data=None):
token = get_token()
url = f"https://api.zoom.us/v2{path}"
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(url, data=body, method=method)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
if resp.status == 204:
return {"message": "OK (no content)"}
result = json.loads(resp.read())
return result
except urllib.error.HTTPError as e:
err_body = e.read().decode("utf-8", errors="replace")
try:
err_json = json.loads(err_body)
return {"error": err_json}
except Exception:
return {"error": err_body}
# =============================================================================
# Actions
# =============================================================================
def list_meetings(user=None, page_size=10, meeting_type="upcoming"):
user = user or ZOOM_USER_ID
result = api_call("GET", f"/users/{urllib.parse.quote(user)}/meetings?page_size={page_size}&type={meeting_type}")
return result
def get_meeting(meeting_id):
return api_call("GET", f"/meetings/{meeting_id}")
def create_meeting(topic, start_time, duration=60, timezone="Asia/Shanghai", password=None):
user = ZOOM_USER_ID
payload = {
"topic": topic,
"type": 2,
"start_time": start_time,
"duration": duration,
"timezone": timezone,
"settings": {
"host_video": True,
"participant_video": True,
"join_before_host": False,
"mute_upon_entry": False
}
}
if password:
payload["password"] = password
return api_call("POST", f"/users/{urllib.parse.quote(user)}/meetings", payload)
def delete_meeting(meeting_id):
return api_call("DELETE", f"/meetings/{meeting_id}")
def get_user(user=None):
user = user or ZOOM_USER_ID
return api_call("GET", f"/users/{urllib.parse.quote(user)}")
def list_users(page_size=30):
return api_call("GET", f"/users?page_size={page_size}")
def list_recordings(user=None, page_size=10):
user = user or ZOOM_USER_ID
return api_call("GET", f"/users/{urllib.parse.quote(user)}/recordings?page_size={page_size}")
def help():
print("""
用法: zoom-s2s.py <action> [参数...]
可用 Action:
list_meetings [user] [page_size] [type] 列出会议 (type: upcoming/past/live)
get_meeting <meeting_id> 获取单个会议详情
create_meeting <topic> <start_time> <duration> [timezone] [password]
创建会议 (start_time: YYYY-MM-DDTHH:MM:SS)
delete_meeting <meeting_id> --yes 删除会议(需 --yes 确认)
get_user [user_id] 获取用户信息
list_users [page_size] 列出账户下所有用户
recordings [user_id] [page_size] 获取云录像
help 显示本帮助
示例:
# 列出即将到来的会议
python3 zoom-s2s.py list_meetings service@uperform.cn 10 upcoming
# 创建明天早上10点会议
python3 zoom-s2s.py create_meeting "煎饼果子讨论会" "2026-05-05T10:00:00" 60 Asia/Shanghai
# 获取云录像
python3 zoom-s2s.py recordings service@uperform.cn 10
""")
# =============================================================================
# Main
# =============================================================================
def main():
load_env()
if len(sys.argv) < 2:
help()
sys.exit(0)
action = sys.argv[1].lower()
args = sys.argv[2:]
if action == "list_meetings":
user = args[0] if len(args) > 0 else ZOOM_USER_ID
page_size = int(args[1]) if len(args) > 1 else 10
meeting_type = args[2] if len(args) > 2 else "upcoming"
result = list_meetings(user, page_size, meeting_type)
elif action == "get_meeting":
if len(args) < 1:
print("❌ 需要 meeting_id", file=sys.stderr)
sys.exit(1)
result = get_meeting(args[0])
elif action == "create_meeting":
if len(args) < 2:
print("❌ 需要 topic 和 start_time", file=sys.stderr)
sys.exit(1)
topic = args[0]
start_time = args[1]
duration = int(args[2]) if len(args) > 2 else 60
timezone = args[3] if len(args) > 3 else "Asia/Shanghai"
password = args[4] if len(args) > 4 else None
result = create_meeting(topic, start_time, duration, timezone, password)
elif action == "delete_meeting":
if len(args) < 1:
print("❌ 需要 meeting_id", file=sys.stderr)
sys.exit(1)
if "--yes" not in args:
print(f"⚠️ 即将删除会议 {args[0]},此操作不可撤销。", file=sys.stderr)
print(f" 请添加 --yes 参数以确认执行,例如:", file=sys.stderr)
print(f" python3 zoom-s2s.py delete_meeting {args[0]} --yes", file=sys.stderr)
sys.exit(1)
result = delete_meeting(args[0])
elif action == "get_user":
user = args[0] if len(args) > 0 else None
result = get_user(user)
elif action == "list_users":
page_size = int(args[0]) if len(args) > 0 else 30
result = list_users(page_size)
elif action == "recordings":
user = args[0] if len(args) > 0 else None
page_size = int(args[1]) if len(args) > 1 else 10
result = list_recordings(user, page_size)
elif action in ("help", "--help", "-h"):
help()
sys.exit(0)
else:
print(f"❌ 未知 action: {action}", file=sys.stderr)
help()
sys.exit(1)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()