Releases: CloudSecurityAlliance/csa-skilljar
Release list
v0.15.0 — the credential guidance was wrong, and now exists
Two fixes, both about credentials reaching the server and the server explaining how.
check_access talked users out of the fix
Its v1 detail said "No v1-backed tools are implemented yet, so this is not currently needed" — while 27 v1-backed tools were shipping. --help carried the same claim.
The routing around it was correct, which is what made it bad: _require_v1 sends a user whose v1 tool just refused to call check_access, so the one message a blocked user was deliberately routed to was the one saying the credential was unnecessary. A silent gap would have been better; they would have kept looking.
A new test derives the contradiction from the live tool registry and fails if any registered tool needs the v1 key while the guidance claims none do — mutation-tested by restoring the original wording. scripts/check_docs.py could never have caught this: it ties documentation to artifacts, and the claim lived in a Python string.
Instructions for obtaining a credential, which did not exist
v1 had none at all; v2 had one vague line. Both now name the Skilljar Dashboard, and the v2 text names scopes — the part people actually get wrong, and the confusing one: this server checks scopes locally and refuses before calling, so an under-scoped client looks like an unsupported tool, and adding a scope needs the client re-issued rather than a restart.
The server also now says plainly that there is no sign-in and no authenticate tool — v2 uses client_credentials, where the credential is the identity. In check_access and in the INSTRUCTIONS the model reads, so the question is answered in the session. Resolves FRICTION-004.
CSA_SKILLJAR_ENV_FILE was set by the installers and ignored by the server
Both csa-skilljar-setup.sh and .ps1 write the credential to an owner-only file and point the registration at it by path. mcp-launch.sh honours that variable — but it is a repo script, is not shipped in the wheel, and is not what the installers register. So the file would be written, chmod 0600, announced as "Skilljar credential installed", and then ignored, with the server reporting no credential. Every step succeeding.
It had not fired only because CSA-Plugins carries no credential yet.
env_with_file() matches mcp-launch.sh's contract exactly so the two cannot drift: an exported variable wins, the file is parsed and never sourced, and only CSA_SKILLJAR_* is taken from it — importing PATH or another service's key would be a privilege-escalation seam, not a convenience.
Also
Recorded in the spec: Skilljar's discovery document advertises only authorization_code and refresh_token and does not list client_credentials, which its token endpoint nevertheless accepts. The natural way to check whether this architecture is supported says no.
Full changelog: https://github.com/CloudSecurityAlliance/csa-skilljar/blob/main/CHANGELOG.md
v0.14.0 — demonstration_plan
The tour that is also the end-to-end test.
demonstration_plan
Follows the CINO DEMO-AS-END-TO-END-TEST.md pattern, marked [proven]. A demonstration and an end-to-end test are the same artifact seen from two sides — and people actually run demonstrations.
Two modes. read_only changes nothing and is safe against the production organization these credentials reach. read_write also creates and deletes content — a course, a quiz, questions, a bank, an empty group — and never a learner, a publication or a credential. Every write step is paired with cleanup.
It returns the plan, not the result. The model calls the tools. Running them inside the tool would block a conversation behind one call and demonstrate nothing — and it would skip the thing no unit test reaches: whether the descriptions are good enough to use from a standing start.
Coverage is computed from the live registry, not maintained, so a tool added next block shows up as a hole. Both modes reach zero gaps across all 112 tools — exercised, excluded with a written reason, or out-of-scope for the mode. Thirty-two exclusions each say why: irreversible PII erasure, credential minting, anything that emails a real person, and get_purchase, which has no listing endpoint so a demo has no id to use.
Learner steps use named accounts only and every learner-facing listing carries a filter — list_vilt_registrations is pinned to one session, because unfiltered it returns hundreds of real names and email addresses.
Live run: 52 of 61 steps, 2 errors, both the scope pre-check refusing correctly and both now predicted up front. The first run predicted zero refusals and hit two, because it checked the capability profile and not the OAuth scopes; prediction now covers both and says which refused — a profile is an environment variable and a restart, a scope needs the credential re-issued.
Also in this release
The [0.13.0] changelog section, which never existed. That release bumped the version string and nothing else, so Blocks 14–17 shipped with no entry at all and Block 14's notes were stranded under [Unreleased]. Reconstructed from the shipped commits and marked as written after the fact rather than backdated silently.
Full changelog: https://github.com/CloudSecurityAlliance/csa-skilljar/blob/main/CHANGELOG.md
v0.13.0 — every roadmap block complete
Every roadmap block is done. 111 tools — 84 over Skilljar's v2 API and 27 over v1 — covering every capability CSA's organization actually uses.
Four blocks in this release.
Learning paths
list_paths · get_path · list_path_items · list_published_paths · list_course_series · list_learner_path_enrollments
Three words that sound alike and are not: a path is a sequence and is invisible on its own; a published path is that sequence on a domain, so a path published twice is two of them with separate URLs and visibility; a course series is an unordered catalogue grouping with no completion. Path items come back in curriculum order with no rank field — so the order is the data, and the description says not to sort.
Webhooks and event payloads
list_webhooks · get_webhook · preview_event_payload
Ten sample-* endpoints became one tool: preview_event_payload(event_type), accepting either spelling of the event name.
Webhook secrets are withheld. Skilljar returns a shared-secret header value, a token in the target URL's query string, and a Basic-auth password in plain text. None of them are returned here — you get header names, and the URL's host and path, which answer where an event goes without handing over the credential.
Instructor-led training
list_ilt_sessions · list_vilt_session_events · list_vilt_registrations · list_ilt_instructors
A session is the class and is not a date; a session event is a scheduled occurrence with a timezone, and is what a learner registers for. list_vilt_registrations carries a learner's name and email on every row, so it needs the people.read capability and warns when called unfiltered.
Taxonomy
list_labels · list_tags · list_course_labels · list_group_categories
Labels are internal and never shown to learners; tags are public and carry a slug used in catalogue URLs. In the data both are just a name, so the distinction lives in the descriptions.
What this release is not
Not v1.0.0. The roadmap set that here, but v1.0.0 should mean the surface is proven — and the write half is not. Every read in this project is exercised against a real Skilljar organization; no write has been. That is stated rather than left to be discovered.
1297 tests. Python 3.10–3.14, Apache-2.0.
v0.12.0 — commerce, read-only
Commerce — promo codes, pools, offers, training credits and purchases. All over Skilljar's v1 API, because v2 has no commerce surface at all.
list_promo_codes·list_promo_code_pools·list_offers·list_training_credit_codes·get_purchase
Read-only, by decision
Nothing here creates, changes or revokes anything. A promo code is money, a mistake is visible to customers, and the useful question is almost always "what exists and is it still valid" rather than "make more". A test asserts the module never grows a write tool.
These need the reporting or full capability profile — deliberately not parity, which mirrors Skilljar's official server, and that server has no commerce tools at all.
Built for scale, because this is where it bites
v1 defaults to 250 rows per page and will honour page_size=1000. The reference organization has 13,708 promo codes across 4,290 pools, so a tool that inherited that default would put thousands of rows into a conversation just to answer "are there any".
Instead: pages default to 25, anything above 250 is refused with a message pointing at total, and every listing reports v1's own count. "How many" and "does this code exist" are both answerable from one small page.
Two things worth knowing
The pool carries the discount, not the code. A promo-code row has only its own usage — use_count against max_uses, where a null max_uses means unlimited rather than none left. "How much is this code worth" is a question about the pool.
Purchases cannot be searched. v1 offers only get_purchase by id — no listing, no filter. The id has to come from a fulfillment webhook or an order reference held elsewhere.
94 tools, 1142 tests. Python 3.10–3.14, Apache-2.0.
v0.11.0 — the asset library
The asset library — the files courses are built from. list_assets and get_asset, over Skilljar's v1 API, because v2 has no assets endpoint at all. This is what resolves the content_asset_id that list_lessons returns.
Read this before using get_asset
get_asset returns a download_url, and that URL is the file, not a reference to it.
It is a presigned S3 link. Verified against the live API with a ranged GET carrying no authorization header: 206, application/pdf. It needs no Skilljar credentials, works for about an hour, and is different every time it is fetched.
So anyone who reads the URL can download the content — and none of this project's controls reach that far: not the capability profile, not the OAuth scope, not the API key. The exposure happens wherever the URL is read. Do not paste one into a transcript, a ticket or a screenshot, and do not store it, because it expires and then reads as a broken asset rather than an expired link.
Three things bound it: the listing carries no link at all, the warning travels in the payload rather than only the description, and it expires. Nothing prevents a model repeating the URL, which is recorded as an accepted risk rather than claimed as handled.
Also
aspect_ratio is not surfaced — it is 16:9 on all 157 assets in the reference organization, PDFs included, so it is a default rather than a measurement. type is returned as asset_type, because every v2 resource carries a JSON:API type.
v1's web-package endpoints are deliberately not added: v2 owns list, get and delete, and putting one capability on two backends with two data shapes is what ADR-002 forbids.
89 tools, 1095 tests. Python 3.10–3.14, Apache-2.0.
v0.10.0 — the second API
The second API. Skilljar has two, and until now this server spoke only to v2.
This release adds V1Backend and the first capability served by it — learner progress:
find_learner— look up a learner by email, and get the id both APIs uselist_learner_progress— how far a learner has got in every course they are enrolled inget_learner_progress— one learner, one course, on one domain
Why v1 at all
ADR-002 governs it: v2 owns every capability v2 has; v1 is used only for what v2 lacks. No fallback, no dual-routing — and that is now a passing test rather than a rule, asserting the two backends' method sets are disjoint.
Learner progress qualifies. v2's enrolment record carries score, status and dates but no lesson counts, no credits, no re-enrolment history — so "how far through this course is this learner, in lessons" is answerable only through v1.
Two things probing found that Skilljar's v1 document does not say
Per-lesson progress does not exist. The documented endpoint returns 404 on the live API, and so does every alternative route. Both progress tools say "COUNTS ONLY, NOT WHICH LESSONS" so it cannot be mistaken for missing data.
The by-id fetch resolves by the underlying course, not the publication. A course published to two domains returns the wrong one, with a 200 and nothing to indicate a substitution — 53 of one learner's 54 enrolments matched, which is what made it dangerous. get_learner_progress selects from the listing instead.
Configuration
The v1 capabilities need CSA_SKILLJAR_V1_API_KEY — a separate credential from the v2 client id and secret. Without it those three tools report the variable to set, and the whole v2 surface keeps working.
87 tools, 1074 tests. Python 3.10–3.14, Apache-2.0.
v0.9.0 — credential administration
Credential administration — the first work past parity.
Skilljar's official MCP server ships the tool that mints an OAuth credential and withholds every tool that audits or remediates one. Through it you can create a client, then cannot list what exists, see what it may do, narrow it, rotate a leaked secret, or turn it off.
This release is that second half: list_oauth_clients, get_oauth_client, list_oauth_scopes, create_oauth_client, update_oauth_client, deactivate_oauth_client, rotate_oauth_client_secret, revoke_refresh_token.
All eight are gated by the admin capability profile — including the reads. Enumerating an organization's credentials is the reconnaissance step, so listing is gated exactly as hard as rotation.
Two things worth knowing
There are now two ways to create a client, and one silently does not work. create_oauth_client is authenticated and bound to your organization. The official server's register_oauth_client uses RFC 7591 dynamic registration, which binds no organization — that client authenticates fine and then reads nothing, forever, with no error saying why. The tool descriptions say so at length.
A revocation cannot be confirmed. RFC 7009 §2.2 requires the endpoint to answer success whether or not the token existed, so a typo and a real revocation are indistinguishable. revoke_refresh_token reports that revocation was requested, never confirmed.
Also
The release workflow now runs its checks before asking for approval. It was one gated job, so a reviewer was asked to approve a publish before any test had run. Split into build (every check, ungated) and publish (upload only, gated), with the artifact handed between them — so the approval is informed, and publish cannot build something different from what passed.
84 tools, 1000 tests. Python 3.10–3.14, Apache-2.0.
v0.8.0 — full parity with Skilljar's official MCP server
First release. Full parity with Skilljar's official MCP server — all 73 of its tools, plus three of our own for server management.
Parity is asserted by tests/test_parity.py, not claimed: the registered tool set is diffed against a registry captured from the live official server, and the test fails if anything is missing.
What it is
A Python library (csa_skilljar) and a local stdio MCP server over Skilljar's REST APIs. It reproduces the official server's tool surface exactly — same names, same arguments — and adds what that server lacks:
- Pagination on every listing that supports it. The official
list_courseshas none at all;page_cursorandpage_sizeare additive extensions here. - Capability profiles. Destructive tools ship present-and-off. The default
parityprofile grants only read capabilities; PII erasure and password tools needfullnamed explicitly. - A local scope pre-check. An impossible call is refused before any network traffic, naming the exact missing OAuth scope.
client_credentials, so no browser. The hosted server is remote and must useauthorization_codewith a consent flow and a token cache. Running locally removes all of that — no redirect URI, no token file on disk.
Install
pipx install csa-skilljarConfigure with CSA_SKILLJAR_V2_CLIENT_ID and CSA_SKILLJAR_V2_CLIENT_SECRET from a Skilljar Dashboard API client, then call check_access first — it is built to work when nothing else does.
scripts/mcp-launch.sh reads credentials from a .env file if you would rather your MCP client configuration held no secret.
What is not proven
No write tool has ever run against a real Skilljar organization. Every write is implemented and tested against an in-memory double, and its semantics are pinned by the captured official registry — but tests/integration/ is enforced read-only, and will stay that way until there is somewhere safe to write.
That blocks confidence, not delivery. It is stated here rather than left to be discovered.
Status
Development Status :: 3 - Alpha. Python 3.10–3.14. Apache-2.0.
Full per-block detail, including the traps this reproduces deliberately and the ones it refuses to reproduce, is in CHANGELOG.md.
Use Skilljar's official MCP server if it covers what you need — it is a good first-party server and they are actively investing in v2. This exists for the gaps.