bewell-catalyst-mcp
MCP server for managing activity libraries on BeWell Catalyst, enabling creation, update, listing, fetching, and deletion of activities with media uploads.
README
bewell-catalyst-mcp
An MCP (Model Context Protocol) server for managing an organization's activity library on the BeWell Catalyst platform. From any MCP-capable Claude session you can create, update, list, fetch and delete library activities of every type, including uploading their media (images, video, audio).
The server contains no business logic: every tool call is a thin RPC to a Catalyst cloud function, where authentication, authorization and domain rules live. The one thing it does locally is read media files from disk and upload them through a signed URL.
What publishing means: a published activity lands in the organization's library and appears immediately in the teachers' Manage Activities screen. Participants see it only once a teacher places it in a course. Publishing stocks the library; it does not push content to participants.
Install
Requires Node.js 20 or newer.
Add to your .mcp.json (Claude Code) or MCP client configuration:
{
"mcpServers": {
"bewell-catalyst": {
"command": "npx",
"args": ["-y", "bewell-catalyst-mcp"],
"env": {
"BEWELL_CATALYST_API_KEY": "bwc_...",
"BEWELL_CATALYST_FUNCTIONS_BASE_URL": "https://europe-west1-bewellit-prod-eu.cloudfunctions.net"
}
}
}
}
Environment variables
| Variable | Purpose |
|---|---|
BEWELL_CATALYST_API_KEY |
API key (bwc_...) minted by a platform admin. Sent in the x-bewell-catalyst-api-key header on every call. One key can manage one or more organizations. |
BEWELL_CATALYST_FUNCTIONS_BASE_URL |
Cloud Functions origin of the target environment (selects the region/project). |
BEWELL_CATALYST_ORG_LOCK (optional) |
Pin this workspace to a single organizationId. When set, every org-scoped tool refuses any other organizationId before making a network call. list_organizations stays available. See Working with multiple organizations. |
| Environment | Base URL |
|---|---|
| EU | https://europe-west1-bewellit-prod-eu.cloudfunctions.net |
| US | https://us-central1-bewellit-prod-us.cloudfunctions.net |
API keys and storage are per-environment: a key minted for the EU environment only works against the EU base URL, and media uploaded in one environment cannot be referenced from the other.
Getting an API key
Keys are minted by a platform administrator in the Catalyst admin UI
(there is no self-serve issuance). A key manages one or more
organizations and carries the publishing scope, which covers every tool
in this server. Keys can expire and can be revoked at any time; treat them
as secrets. If you don't know which organizationId values a key manages,
call list_organizations.
Tools (15)
| Tool | Purpose |
|---|---|
upload_media |
Upload a local file, get back the finalUrl for media fields |
create_text_activity / update_text_activity |
Markdown articles, optional hero image and journal prompts |
create_video_activity / update_video_activity |
Video activities (uploaded file + duration + thumbnail) |
create_audio_activity / update_audio_activity |
Audio activities such as guided meditations |
create_image_activity / update_image_activity |
Full-screen image activities |
create_simple_activity / update_simple_activity |
Config-only cards: mindful_timer, breathing, cold_plunge, copyright |
get_activity |
Fetch one activity by slug (returns activity: null when absent) |
list_activities |
Paginated summaries, optional type filter, no content bodies |
delete_activity |
Delete an MCP-created activity plus (best-effort) its media |
list_organizations |
List the organizations this key manages ({id, name}); takes no inputs |
Identity and idempotency
Activities are addressed by (organizationId, contentSlug). The slug
namespace is shared across all types: one slug names exactly one
activity per organization, and its type can never change after
creation. Creating with an existing slug (same type, MCP-created) acts as
an update; retries are safe.
Only activities created through this MCP can be updated or deleted.
Activities authored in-app are readable via get_activity /
list_activities but never writable here.
Working with multiple organizations
A single key can manage several organizations. There is still one key and
one config entry — you choose which organization a call targets purely
through the organizationId argument on each tool. Switching organizations
means passing a different organizationId; there is no config edit and no
restart.
- Discover what a key manages with
list_organizations(no inputs). It returns{ "organizations": [{ "id": "...", "name": "..." }, ...] }(nameis the display name, ornullif the record is unavailable). Start here whenever you don't already know theorganizationIdto use. - Target an organization by passing one of those ids as
organizationIdto any other tool.
The server enforces membership on every call: a key may only act on the
organizations attached to it. A call for an unattached organization is
refused with PERMISSION_DENIED — call list_organizations to see the
valid ids.
Locking a workspace to one organization (recommended)
When a workspace should only ever touch one organization, set
BEWELL_CATALYST_ORG_LOCK to that organization's id. Every org-scoped tool
then refuses any other organizationId client-side, before any network
call, naming both the locked and the attempted organization.
list_organizations remains available (read-only discovery is harmless).
The recommended pattern is one .mcp.json entry per project, each
locked to that project's organization — so a Claude session working in that
project physically cannot drift to another organization even though the key
could reach several:
{
"mcpServers": {
"bewell-catalyst": {
"command": "npx",
"args": ["-y", "bewell-catalyst-mcp"],
"env": {
"BEWELL_CATALYST_API_KEY": "bwc_...",
"BEWELL_CATALYST_FUNCTIONS_BASE_URL": "https://europe-west1-bewellit-prod-eu.cloudfunctions.net",
"BEWELL_CATALYST_ORG_LOCK": "your-org-id"
}
}
}
}
Multi-organization sessions (rare, operator-driven) simply omit the lock. The lock is a convenience guard against accidental cross-organization writes; the server-side membership check remains the hard boundary either way.
Per-type contracts
Common required fields: organizationId, contentSlug, name.
Common optional fields: description, tags, author, analyticsId,
isPractice, includeInAiChat.
| Type | Additionally required | Optional extras | Notes |
|---|---|---|---|
text |
contentMarkdown (max 50k chars) |
imageUrl (hero), imagePosition (top/bottom), journalPrompts |
|
video |
contentUrl, durationSeconds |
thumbnailUrl (strongly recommended), journalPrompts |
Directly streamable file (mp4/webm); a YouTube page URL will not play |
audio |
contentUrl, durationSeconds |
thumbnailUrl, journalPrompts |
The card shows "(0 seconds)" without a real duration |
image |
imageUrl |
markdown description |
Full-screen viewer |
copyright |
description (the notice text) |
none | Served by the *_simple_activity tools |
mindful_timer |
none | none | Name-only config card |
breathing |
none | none | Breathing pattern is a user choice at runtime |
cold_plunge |
none | none | Name-only config card |
durationSeconds is a positive integer (max 86400) and is not probed
server-side: measure the real duration locally (for example with
ffprobe) and pass it.
analyticsId enables completion tracking and is write-once: it can be
set on create (recommended value: the contentSlug) or added later, but
never changed or cleared once set. Without it the activity is untracked.
Media workflow
Always upload_media first, then pass the returned finalUrl into the
create/update call. Media URL fields only accept finalUrl values minted
for your own organization and environment: external URLs are rejected.
| Role | Feeds | Formats | Max size |
|---|---|---|---|
video |
contentUrl (video) |
.mp4 .mov .webm |
500 MB |
audio |
contentUrl (audio) |
.mp3 .m4a .aac .wav .ogg |
50 MB |
thumbnail |
thumbnailUrl (video/audio) |
.jpg .png .webp |
5 MB |
image |
imageUrl (image activity, text hero) |
.jpg .png .webp .gif |
2 MB |
inline |
images inside contentMarkdown |
.jpg .png .webp .gif |
2 MB |
filePath must be a local path on the machine running the MCP server. The
MIME type is inferred from the extension (pass contentType explicitly to
override). Re-uploading with the same role and the same file extension
overwrites the previous file; the same role with a different extension
creates a sibling file instead. That is harmless: only the finalUrl you
pass to create/update is referenced, and delete_activity's folder
cleanup collects any orphans. inline creates a new file per call.
Update semantics (PATCH)
update_* tools patch, they do not replace:
- Omitted optional field: the stored value is preserved.
- Explicit
null: the stored value is cleared (tags: []clears tags). nameand the type's required fields (for examplecontentUrl+durationSecondsfor video) must be re-sent on every update.analyticsId: omit to preserve. Changing or clearing an existing value is refused.typeis immutable; the update tools re-assert it and the server verifies it matches.
Deleting
delete_activity removes the activity document and best-effort deletes its
uploaded media. Two caveats:
- Placements are not checked. If a teacher has placed the activity in a course template or course, deleting it leaves a dangling reference (the app renders nothing for it). Check with the teacher before deleting anything that may be in use.
- Only MCP-created activities can be deleted.
Deletion is idempotent: a missing slug returns
{"deleted": false, "reason": "not-found"} without error. In a successful
response, mediaCleanedUp: false means the document was deleted but the
media folder could not be cleaned up.
Error glossary
| Status | Meaning | What to do |
|---|---|---|
INVALID_ARGUMENT |
A field failed validation; the message names it | Fix the named field. For "first-party media URL" errors, use upload_media and pass its finalUrl |
FAILED_PRECONDITION "not created by MCP" |
The activity was authored in-app | Leave it alone; create a new slug for your own version |
FAILED_PRECONDITION "type is immutable" |
The slug is taken by another type | Choose a new contentSlug |
FAILED_PRECONDITION "analyticsId is immutable" |
Attempt to change/clear a set analyticsId |
Omit analyticsId from the update |
UNAUTHENTICATED |
Key missing, malformed, revoked or expired | Check BEWELL_CATALYST_API_KEY; ask a platform admin for a new key |
PERMISSION_DENIED |
The organizationId is not one this key manages, or the key is missing scope |
Call list_organizations and pass a listed id |
RESOURCE_EXHAUSTED |
Rate limit (100/min per key, 30/min per IP) | Wait a minute, retry |
INTERNAL |
Server-side failure | Retry once; report if persistent |
Development
npm install
npm run build # tsc -> dist/
npm run lint # eslint
npm test # vitest
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。