Skip to content

Releases: CloudSecurityAlliance/csa-skilljar

v0.15.0 — the credential guidance was wrong, and now exists

Choose a tag to compare

@kurtseifried kurtseifried released this 31 Aug 18:22
0af9fac

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

Choose a tag to compare

@kurtseifried kurtseifried released this 31 Aug 03:11
ed97ee3

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

Choose a tag to compare

@kurtseifried kurtseifried released this 29 Aug 21:37
7ab20d8

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

Choose a tag to compare

@kurtseifried kurtseifried released this 28 Aug 22:58
3bd596b

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

Choose a tag to compare

@kurtseifried kurtseifried released this 28 Aug 22:03
b4be56b

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

Choose a tag to compare

@kurtseifried kurtseifried released this 28 Aug 19:30
1cc4774

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 use
  • list_learner_progress — how far a learner has got in every course they are enrolled in
  • get_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

Choose a tag to compare

@kurtseifried kurtseifried released this 28 Aug 06:31
4adbca4

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

Choose a tag to compare

@kurtseifried kurtseifried released this 28 Aug 00:20
c42be60

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_courses has none at all; page_cursor and page_size are additive extensions here.
  • Capability profiles. Destructive tools ship present-and-off. The default parity profile grants only read capabilities; PII erasure and password tools need full named 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 use authorization_code with 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-skilljar

Configure 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.