Skip to main content
project_tracker_base: CINO Project Tracker:appf7fRQUvY9Iy7sL
project_tracker_table: Projects:tblchmbxSAavvJKaY
project_tracker_record: csa-skilljar:recbwfx6O30BboQNv
project_source: github:CloudSecurityAlliance-Internal/CINO-Projects/projects/CloudSecurityAlliance/csa-skilljar

csa-skilljar

A Python library and local MCP server for the Skilljar customer education platform, covering both of Skilljar's REST APIs — v1 and v2 — behind one set of tools.

Status: all seventeen blocks complete. Full 73-tool parity with Skilljar's official server, plus every capability that exists only in v1. v0.15.0 on PyPI.

112 tools — 84 over Skilljar's v2 API, twenty-seven over v1, and demonstration_plan.

pipx install csa-skilljar
Server check_access · describe_capabilities · report_a_problem · demonstration_plan — a guided tour that is also the end-to-end test, in read-only or read/write mode
Courses list_courses · get_course · create_courses · update_courses
Lessons list_lessons · get_lesson · create_lessons · update_lessons
Quizzes list_quizzes · get_quiz · create_quizzes · update_quizzes · delete_quizzes
Questions list_questions · get_question · create_questions · update_questions · delete_questions
Question banks list_question_banks · get_question_bank · create_question_banks · update_question_banks · delete_question_banks
Bank bindings list_quiz_question_bank_assignments · bind_quiz_question_banks · update_quiz_question_banks · unbind_quiz_question_banks
Enrolment list_enrollments · get_enrollment · update_enrollments · complete_enrollments · bulk_enroll_students
Reporting list_certificates · get_certificate · get_course_analytics · list_course_ratings
Students list_students · get_student · create_students · update_students
Groups list_groups · get_group · create_groups · update_groups · add_group_memberships · remove_group_memberships
Signup fields list_signup_field_values · get_signup_field_value · create_signup_field_values · update_signup_field_values
Publishing list_published_courses · get_published_course · publish_courses · update_published_courses
Catalog list_domains · get_domain
Course visibility list_visibility_overrides · add_visibility_overrides · remove_visibility_overrides
Web packages list_web_packages · get_web_package · create_web_packages · update_web_packages
Students (destructive) anonymize_student · deactivate_student · set_student_password · send_password_reset — gated on people.destructive, which no profile but full grants
Groups (destructive) delete_groups — a hard delete that cascades to memberships and course visibility; gated on groups.delete
Publishing (public-facing) unpublish_published_course · republish_published_course · delete_published_course — gated on publishing.write, which authoring does not grant
Web packages (destructive) delete_web_package — refused while a live lesson uses the package
Learner progress (v1) find_learner · list_learner_progress · get_learner_progress — lesson counts, credits and re-enrolment history, which v2 does not carry. Needs CSA_SKILLJAR_V1_API_KEY
Assets (v1) list_assets · get_asset — the files courses are built from. v2 has no assets endpoint at all
Commerce (v1, read-only) list_promo_codes · list_promo_code_pools · list_offers · list_training_credit_codes · get_purchase — v2 has no commerce surface at all
Learning paths (v1) list_paths · get_path · list_path_items · list_published_paths · list_course_series · list_learner_path_enrollments — v2 has no path surface at all
Webhooks (v1) list_webhooks · get_webhook · preview_event_payload — webhook secrets are withheld; one tool covers all ten event types
Instructor-led (v1) list_ilt_sessions · list_vilt_session_events · list_vilt_registrations · list_ilt_instructors
Taxonomy (v1) list_labels · list_tags · list_course_labels · list_group_categories — labels are internal, tags are public
Credentials register_oauth_client · list_oauth_clients · get_oauth_client · create_oauth_client · update_oauth_client · deactivate_oauth_client · rotate_oauth_client_secret · list_oauth_scopes · revoke_refresh_token — all off unless the admin profile is named

All 73 official tools are present — asserted by tests/test_parity.py, not claimed. The three extra tools are our own server management. See ROADMAP.md.

Start with Skilljar's official MCP server

If you want Skilljar in an AI client, use Skilljar's own MCP server. That is the right default and we recommend it without reservation. It is first-party, hosted and maintained by the vendor, needs nothing installed on your machine, covers the whole v2 API in 73 tools, and authenticates with OAuth and per-operation scopes. Skilljar are actively building v2 out, so it gets better on their release cadence rather than ours.

claude mcp add skilljar --transport http https://mcp.skilljar.com/mcp

Try that first. For most people it is the whole answer.

…but if you need more

Some things are not in the v2 API yet, so no v2 client can reach them. The v1 API is considerably larger — 340 operations against v2's 82 — and today it is the only way to get at:

  • per-lesson learner progress — v2 reports course-level completion only
  • webhooks — v2 has no event notifications at all
  • asset upload — v2 has no file upload
  • learning paths, instructor-led training, and the commerce stack (offers, promo codes, purchases, training credits)

csa-skilljar exists for that gap. It reproduces the official tool surface exactly — same tool names, same argument names — and then adds the v1-only capabilities alongside them, so you do not have to choose between the two APIs or run two servers.

It also runs locally over stdio, which some organisations need: your API credentials stay on your own machine.

We expect this project to shrink over time, and that is the intended outcome. Skilljar has publicly reserved OAuth scopes for webhooks, paths, assets, tags and commerce. As those endpoints ship, the corresponding v1 support here gets retired in favour of v2 — the tool names stay the same and callers notice nothing.

Official Skilljar MCP csa-skilljar
APIs v2 v1 + v2
Transport remote HTTP local stdio
Credentials held server-side stay on your machine
Auth OAuth authorization code (browser) OAuth client credentials + v1 API key
Capability control OAuth scopes at consent scopes plus per-install profiles
Library — the library is the product too

Checking the state of Skilljar's v2 API

Skilljar's v2 API is actively growing, and the official MCP server tracks it closely. That is good news for everyone — and it means the coverage map in this project has a shelf life. Before assuming a gap documented here is still a gap, re-check upstream.

# 1. The published v2 surface
curl -s https://api.skilljar.com/v2/openapi.json | jq '.paths | keys | length'

# 2. The declared scope catalogue — a leading indicator of what is coming
curl -s https://api.skilljar.com/.well-known/oauth-authorization-server \
  | jq -r '.scopes_supported[]'

# 3. The official MCP server's live tool list
#    Connect it, then run /mcp in Claude Code.

At the time of writing, the scope catalogue advertises 88 scopes while the published v2 spec uses 28 — areas including webhooks, paths, assets, tags and commerce have scopes reserved but no endpoints yet. When those endpoints ship, the v1 fallbacks this project provides for them should be retired in favour of v2, and this note updated.

scripts/check_upstream.py will automate all three and report drift against the snapshots in specs/. Until then, run the commands above.

Installation

Three routes, all installing the same package from PyPI. Pick by what you already use.

pipx install csa-skilljar          # recommended
uv tool install csa-skilljar       # if you already use uv

Both put a csa-skilljar-mcp executable on your PATH and keep the package's dependencies in their own virtual environment. That isolation is the point rather than a nicety: this server requires mcp>=2.1, and a machine with mcp 1.x installed globally is exactly the environment that produces the most common MCP failure — 1.x still has mcp.server.fastmcp, so the wrong version looks plausible right up until it doesn't.

A plain pip install into a system or user Python is the one path we do not support, for that reason. Inside a virtual environment you control, it is fine.

Confirm what landed, and where:

csa-skilljar-mcp --version
command -v csa-skilljar-mcp        # pipx: ~/.local/bin  ·  uv: ~/.local/bin or `uv tool dir`

If the command is not found, the install directory is not on your PATH yet — pipx ensurepath or uv tool update-shell, then open a new shell. Register the MCP server with an absolute path either way (see below), so this does not matter to the client.

CSA staff: DesktopSetup installs it for you

Members of Cloud Security Alliance do not need any of the above. DesktopSetup installs and registers this server as part of its normal run, on macOS, Linux and Windows:

# macOS / Linux
bash -c "$(curl -fsSL -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/CloudSecurityAlliance/DesktopSetup/HEAD/scripts/macos-ai-tools.sh)"

It installs via pipx, registers the server with CSA_SKILLJAR_PROFILE=parity — read-only capabilities — and leaves an existing registration alone if you have already narrowed it. The step is gated on membership of a private CSA repository, so it does nothing for anyone else and prints nothing.

It does not ship a credential, and that is deliberate rather than an omission. Skilljar uses the client_credentials grant, so a credential is the organization identity — there is no per-user layer, and anyone holding a client secret acts as CSA rather than as themselves. Distributing one to every desktop would be a shared organization key, which is a different thing from the shared app identity that Google Workspace's OAuth client is. So the server installs, registers and starts, and every tool then reports the setup step it needs. Ask in #cino for an API client.

Credentials

Two independent credentials, both optional. The server starts with either, both, or neither, and tells you what is available.

Variable For Obtain from
CSA_SKILLJAR_V1_API_KEY the v1 API Skilljar Dashboard — see Skilljar's API guide
CSA_SKILLJAR_V2_CLIENT_ID / CSA_SKILLJAR_V2_CLIENT_SECRET the v2 API Skilljar Dashboard, v2 API clients

We link Skilljar's own documentation rather than transcribing their dashboard navigation, which we cannot keep current.

Connecting it to an MCP client

If you installed with pipx or uv, keep the secret out of the client config by putting it in a file and naming the file:

mkdir -p ~/.csa_skilljar && chmod 700 ~/.csa_skilljar
cat > ~/.csa_skilljar/skilljar.env <<'EOF'
CSA_SKILLJAR_V2_CLIENT_ID=...
CSA_SKILLJAR_V2_CLIENT_SECRET=...
CSA_SKILLJAR_V1_API_KEY=...
EOF
chmod 600 ~/.csa_skilljar/skilljar.env

claude mcp add csa-skilljar --scope user \
  -e CSA_SKILLJAR_PROFILE=parity \
  -e CSA_SKILLJAR_ENV_FILE="$HOME/.csa_skilljar/skilljar.env" \
  -- "$HOME/.local/bin/csa-skilljar-mcp"

This matters more than it looks: ~/.claude.json is world-readable on a default macOS install (mode 644), and it is not gitignored. Naming a 0600 file keeps the credential out of it and out of your shell history, and makes rotation one file edit rather than a re-registration. An already-exported variable still wins over the file, so an override needs no edit.

You can pass the credentials directly instead — -e CSA_SKILLJAR_V2_CLIENT_ID=... and so on — accepting that the literal secret is then written into ~/.claude.json.

If you are working from a source checkout, scripts/mcp-launch.sh does the same thing against the repository's own .env:

claude mcp add csa-skilljar -- /abs/path/to/csa-skilljar/scripts/mcp-launch.sh

Both the launcher and the server parse the file rather than sourcing it: source executes it, so a stray echo would print to stdout and corrupt the JSON-RPC stream before the server ever starts. Only CSA_SKILLJAR_* names are read from it, so pointing at a general-purpose .env cannot import PATH or another service's key.

Use an absolute path to the executable. A bare csa-skilljar-mcp resolves through PATH, which may find a different install — and the client may not have your shell's PATH at all.

Then call check_access first — it is built to work when nothing else does, and reports which credentials resolved and which scopes the token carries.

There is no login step and no browser. The v2 credential is a machine credential: you create an API client in the Skilljar Dashboard, put its id and secret in your MCP client's configuration, and the server obtains its own access token on first use (client_credentials, ADR-003). No redirect URI, no consent screen, no token file on disk. Skilljar's own hosted MCP server does use an interactive flow — it is remote and acts for a browser user, which is exactly the constraint running locally removes.

Scope the v2 client to what you actually need. The API declares a required scope on every operation, and the sensitive ones are separable — students:anonymize (irreversible), students:deactivate, and students:manage-password can all be withheld from a client used for content authoring.

What it will cover

Reproduces all 73 official tools, then adds v1-only families in this order:

  1. Learner progress — per-lesson detail, which v2 does not report
  2. Assets & media — v2 has no file upload
  3. Commerce — offers, promo codes, purchases, training credits
  4. Learning paths — paths, path items, path enrolments
  5. Events & webhooks — subscriptions and payload previews
  6. Instructor-led training — sessions, instructors, registrations
  7. Labels & tags

Deliberately out of scope: catalog page-building, webhook receiving, caching, and cross-API composite writes. Reasons are in the spec.

Project documentation

File What it answers
Design spec Architecture, routing rule, credential model, auth error taxonomy, phasing. Start here.
ROADMAP.md The block sequence — what ships in what order, and what is parked
GOALS.md What success looks like and how we would know it failed
BUSINESS-CASE.md Why CSA is investing, and the honest case that this project should shrink
TODO.md Index of all open work
DECISIONS-ADR.md Technical decisions and why the rejected alternatives lost
DECISIONS-PRD.md Scope, audience, and what is deliberately out
SECURITY-RESOURCES.md Exposure surface, prompt-injection risk, credential custody
DATA-RESOURCES.md What data this handles, and what it deliberately never stores
WAITING-FOR.md External conditions we are waiting on, each with an observable trigger
FRICTION.md Work that is harder than it should be — including how this project works with AI
RACI.md Who decides what
CLAUDE.md Behavioural contract for AI agents working here

Development

Always use a virtual environment. The interpreter is pinned by .python-version.

python3 -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"

.venv/bin/python -m pytest -q          # offline suite: no network, no credentials
.venv/bin/ruff check src tests scripts
.venv/bin/mypy

./scripts/verify.sh                    # or just this: everything CI checks

Commands are written .venv/bin/... deliberately — a bare pytest resolves to whatever is on PATH, which is how a suite passes against the wrong dependency versions.

Contributions follow CSA's public repo standards: branch and PR for every change, required CI gates, no direct pushes to main.

Releasing

Publishing uses PyPI Trusted Publishing — GitHub Actions authenticates over OIDC and proves its identity with the repository, workflow and environment it is running in. There is no API token anywhere: not in the repository, not in a GitHub secret, not in a .pypirc. Nothing to leak, rotate, or accidentally commit.

The identity PyPI checks is exactly this:

Field Value
PyPI project csa-skilljar
Owner CloudSecurityAlliance
Repository csa-skilljar
Workflow release.yml
Environment pypi

One-time setup (a person with the PyPI account, per RACI.md — credential and publishing identity are not delegated): at https://pypi.org/manage/account/publishing/, add a pending publisher with the five values above. "Pending" is the form used when the project does not exist on PyPI yet; it becomes a normal trusted publisher on first upload.

Each release:

# 1. Bump the single source of truth and refresh the editable install.
#    src/csa_skilljar/__init__.py  __version__ = "X.Y.Z"
.venv/bin/python -m pip install -e ".[dev]"
./scripts/verify.sh

# 2. Merge, then tag from main. The tag MUST equal the packaged version - the
#    workflow refuses to publish when they disagree, rather than shipping a
#    mislabelled artifact.
git tag vX.Y.Z && git push origin vX.Y.Z
gh release create vX.Y.Z --notes-from-tag

Publishing the GitHub release starts release.yml, which reruns the tests, pip-audit and bandit, checks the tag against the packaged version, builds, and refuses to upload an artifact containing anything matching .env, token, secret, credential, analysis/ or docs-html/, or missing py.typed.

It then waits: the publish job's pypi environment has a required reviewer, so the upload does not happen until a human approves it in the Actions run.

The split into two jobs is deliberate. An environment gates a whole job, so a single gated job asked for approval before any test ran. Now build does every check ungated and uploads the artifact; publish downloads that exact artifact and does nothing but upload it. The reviewer approves something already built and verified, and publish has no build step that could produce something different.

Worth knowing that GitHub creates a missing environment unprotected on first use — so environment: pypi in a workflow is a claim, not a control, until the environment actually exists with rules on it.

Licence

Apache-2.0.

Acknowledgements

Skilljar is a Gainsight product. This project is not affiliated with or endorsed by Skilljar or Gainsight; it is an independent client built against their public APIs. The API snapshots in specs/ are fetched from Skilljar's published, publicly accessible OpenAPI documents.

Metadata

Release files for csa-skilljar 0.16.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for csa-skilljar 0.16.0
File Size Uploaded Provenance
csa_skilljar-0.16.0.tar.gz 279.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for csa-skilljar 0.16.0
File Interpreter ABI Platform
csa_skilljar-0.16.0-py3-none-any.whl Python 3 none any Details

Total release size: 443.8 kB

Release files / csa_skilljar-0.16.0.tar.gz

Download URL csa_skilljar-0.16.0.tar.gz
Size 279.9 kB
Tags Source
SHA-256 checksum
How to use checksums
7a31d94f2767c6c2e4b56170947d1e7553b7c4af1a97d440eb0e53073816e37c
BLAKE2b-256 checksum
How to use checksums
132b9c1fc0b411c8fc090e3dac3314c9a9e3ff8bdf33c3b07c337cb0e30145ec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / csa_skilljar-0.16.0-py3-none-any.whl

Download URL csa_skilljar-0.16.0-py3-none-any.whl
Size 163.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7f6e75a32e1d7be746ba66c57fc55b138b7466fe0986be6a65f4571e94ef5872
BLAKE2b-256 checksum
How to use checksums
a7437bc2b88d6d289521c2bb597eab99287252c451234df69b82bec8afe769c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Security for csa-skilljar 0.16.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for csa_skilljar-0.16.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page