Skip to the content.

Quick reference (TL;DR for agents)

For a quick intro, see Skool MCP (concepts and quick setup). This page is the production deployment guide.

Architecture

AI client                    MCP server                 Apify actor             Skool
─────────                    ──────────                 ───────────             ─────
[Claude Desktop]
[Cursor]            ◄─ stdio ─►  [skool-mcp]  ─POST─►  [run-sync-get-     ─►   [api.skool.com]
[Cline]              or HTTP                            dataset-items]
                                       │                       │                     │
                                       │                       ├── api.skool.com ────┤
                                       │                       │     (cookies +      │
                                       │                       │     WAF + buildId)  │
                                       │                       │                     │
                                       ◄──────  result  ────────────────────────────┘

The MCP server is a thin proxy. All Skool complexity stays in the actor — your server just maps MCP tool calls to actor action strings.

Full server implementation — Python

# server.py — Skool MCP server (stdio mode)
import os
import asyncio
import logging
import httpx
import json
from typing import Any
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

logging.basicConfig(level=logging.INFO)
log = logging.getLogger("skool-mcp")

APIFY_TOKEN = os.environ["APIFY_TOKEN"]
SKOOL_EMAIL = os.environ.get("SKOOL_EMAIL")          # for cookie rotation
SKOOL_PASSWORD = os.environ.get("SKOOL_PASSWORD")
GROUP_SLUG = os.environ["SKOOL_GROUP_SLUG"]

_cookies = os.environ.get("SKOOL_COOKIES", "")        # mutable, rotates on WAF_EXPIRED

ACTOR_URL = (
    "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/"
    "run-sync-get-dataset-items"
    f"?token={APIFY_TOKEN}&build=latest&timeout=90"
)

# ---- TOOL CATALOG ----
TOOLS = [
    Tool(
        name="skool_posts_list",
        description="List posts in the Skool community feed. Optional filters: page, sort, limit, since, until, unanswered, labelId.",
        inputSchema={
            "type": "object",
            "properties": {
                "page": {"type": "integer"},
                "limit": {"type": "integer"},
                "sort": {"type": "string"},
                "since": {"type": "string", "format": "date-time"},
                "labelId": {"type": "string"}
            }
        }
    ),
    Tool(
        name="skool_posts_create",
        description="Create a new top-level post. Skool ignores HTML/markdown — pass plain text.",
        inputSchema={
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "content": {"type": "string"},
                "labelId": {"type": "string"}
            },
            "required": ["title", "content"]
        }
    ),
    Tool(
        name="skool_posts_create_comment",
        description=(
            "Reply to a post or comment. For top-level comment: rootId == parentId == postId. "
            "For nested reply: rootId == postId (the post), parentId == commentId (the comment you're replying to)."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "rootId": {"type": "string"},
                "parentId": {"type": "string"},
                "content": {"type": "string"}
            },
            "required": ["rootId", "parentId", "content"]
        }
    ),
    Tool(
        name="skool_members_pending",
        description="List pending member applications. Returns memberId for each.",
        inputSchema={"type": "object", "properties": {}}
    ),
    Tool(
        name="skool_members_approve",
        description="Approve a pending member by memberId. Note: memberId from members:pending, NOT user id.",
        inputSchema={
            "type": "object",
            "properties": {"memberId": {"type": "string"}},
            "required": ["memberId"]
        }
    ),
    Tool(
        name="skool_members_reject",
        description="Reject a pending member by memberId.",
        inputSchema={
            "type": "object",
            "properties": {"memberId": {"type": "string"}},
            "required": ["memberId"]
        }
    ),
    Tool(
        name="skool_members_batch_approve",
        description="Approve multiple pending members in one call. Bypasses the per-call rate limit because Skool handles batch server-side.",
        inputSchema={
            "type": "object",
            "properties": {"memberIds": {"type": "array", "items": {"type": "string"}}},
            "required": ["memberIds"]
        }
    ),
    Tool(
        name="skool_classroom_list_courses",
        description="List all courses in the community classroom.",
        inputSchema={"type": "object", "properties": {}}
    ),
    Tool(
        name="skool_classroom_get_tree",
        description="Get the full tree (folders + pages) of a specific course.",
        inputSchema={
            "type": "object",
            "properties": {"courseId": {"type": "string"}},
            "required": ["courseId"]
        }
    ),
    Tool(
        name="skool_classroom_create_page",
        description="Create a new page inside a course or folder.",
        inputSchema={
            "type": "object",
            "properties": {
                "courseId": {"type": "string"},
                "parentId": {"type": "string", "description": "Course id for top-level page, folder id for nested"},
                "title": {"type": "string", "maxLength": 50}
            },
            "required": ["courseId", "parentId", "title"]
        }
    ),
    Tool(
        name="skool_classroom_set_body",
        description="Set the body of a course page. Markdown auto-converts to TipTap (Skool's internal format).",
        inputSchema={
            "type": "object",
            "properties": {
                "pageId": {"type": "string"},
                "title": {"type": "string"},
                "bodyMarkdown": {"type": "string"}
            },
            "required": ["pageId", "title", "bodyMarkdown"]
        }
    ),
    Tool(
        name="skool_groups_set_auto_dm",
        description="Set the Auto DM message new members receive on join. Max 300 chars. Tokens: #NAME#, #GROUPNAME#.",
        inputSchema={
            "type": "object",
            "properties": {"message": {"type": "string", "maxLength": 300}},
            "required": ["message"]
        }
    ),
]

ACTION_MAP = {
    "skool_posts_list": "posts:list",
    "skool_posts_create": "posts:create",
    "skool_posts_create_comment": "posts:createComment",
    "skool_posts_get_comments": "posts:getComments",
    "skool_posts_get_comments_full": "posts:getCommentsFull",
    "skool_members_pending": "members:pending",
    "skool_members_approve": "members:approve",
    "skool_members_reject": "members:reject",
    "skool_members_batch_approve": "members:batchApprove",
    "skool_events_list": "events:list",
    "skool_events_upcoming": "events:upcoming",
    "skool_classroom_list_courses": "classroom:listCourses",
    "skool_classroom_get_tree": "classroom:getTree",
    "skool_classroom_create_page": "classroom:createPage",
    "skool_classroom_set_body": "classroom:setBody",
    "skool_classroom_update_resources": "classroom:updateResources",
    "skool_files_upload_image": "files:uploadImage",
    "skool_files_upload_file": "files:uploadFile",
    "skool_groups_set_auto_dm": "groups:setAutoDM",
    "skool_system_debug": "system:debug",
}

async def rotate_cookies() -> str:
    """Call auth:login and return new cookies. Used when WAF_EXPIRED."""
    global _cookies
    if not SKOOL_EMAIL or not SKOOL_PASSWORD:
        raise RuntimeError("SKOOL_EMAIL/SKOOL_PASSWORD not set — cannot auto-rotate cookies")
    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.post(ACTOR_URL, json={
            "action": "auth:login",
            "email": SKOOL_EMAIL,
            "password": SKOOL_PASSWORD,
            "groupSlug": GROUP_SLUG,
        })
        data = resp.json()
        result = data[0] if isinstance(data, list) else data
        if not result.get("success"):
            raise RuntimeError(f"auth:login failed: {result}")
        _cookies = result["cookies"]
        log.info("Rotated Skool cookies (expires %s)", result.get("expiresAt"))
        return _cookies

async def call_actor(action: str, params: dict) -> dict:
    global _cookies
    payload = {"action": action, "cookies": _cookies, "groupSlug": GROUP_SLUG, "params": params}
    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.post(ACTOR_URL, json=payload)
        data = resp.json()
        result = data[0] if isinstance(data, list) else data
        if not result.get("success") and result.get("errorCode") == "WAF_EXPIRED":
            log.info("Cookies expired — rotating")
            await rotate_cookies()
            payload["cookies"] = _cookies
            resp = await client.post(ACTOR_URL, json=payload)
            data = resp.json()
            result = data[0] if isinstance(data, list) else data
        return result

app = Server("skool")

@app.list_tools()
async def list_tools():
    return TOOLS

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    action = ACTION_MAP.get(name)
    if not action:
        return [TextContent(type="text", text=json.dumps({"success": False, "error": f"Unknown tool: {name}"}))]
    result = await call_actor(action, arguments)
    return [TextContent(type="text", text=json.dumps(result, indent=2))]

if __name__ == "__main__":
    asyncio.run(stdio_server(app))

Dockerfile

FROM python:3.11-slim
RUN pip install mcp httpx
COPY server.py /app/server.py
WORKDIR /app
CMD ["python", "server.py"]

Build + run:

docker build -t skool-mcp .
docker run -i --rm \
  -e APIFY_TOKEN=... \
  -e SKOOL_EMAIL=... \
  -e SKOOL_PASSWORD=... \
  -e SKOOL_COOKIES=... \
  -e SKOOL_GROUP_SLUG=... \
  skool-mcp

For HTTP/SSE deployment instead of stdio, swap stdio_server for an HTTP transport (mcp.server.sse_server) and expose port 8000. Deploy to Cloud Run / Fly.io / Railway with gcloud run deploy or equivalent.

Connecting from Claude Desktop

{
  "mcpServers": {
    "skool": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "APIFY_TOKEN=...",
        "-e", "SKOOL_EMAIL=...",
        "-e", "SKOOL_PASSWORD=...",
        "-e", "SKOOL_COOKIES=...",
        "-e", "SKOOL_GROUP_SLUG=...",
        "skool-mcp"
      ]
    }
  }
}

Production gotchas


Deploy your Skool MCP server today

→ Open the Skool All-in-One API actor on Apify

Wrap it with the ~150-line MCP server above. Your AI agents get Skool as a native toolset. Pay-per-event (~$0.005-$0.01 per call).

No Skool community to connect yet? Launch one in 10 minutes — 14-day free trial.