MCP 2.5 Release Notes
Release date: October 2026
MCP 2.5 brings Continuous Control Monitoring (CCM) to the JupiterOne MCP Server. Six new tools take the server from 32 tools to 38. Your assistant can now read compliance status the way CCM reports it, by framework, requirement, and control, and explain why a control passes or fails. It can also record, renew, and revoke attestations on live controls.
Before 2.5 the MCP server had no compliance tools. An assistant could only approximate CCM status with J1QL queries over the ccm_* entities, and it had no way to record an attestation.
Remote server users: no. The hosted server runs 2.5, so the compliance tools appear in your connector's tool list. A client that caches the tool list picks them up when it next refreshes it.
Local server users: nothing beyond your usual update. npx @jupiterone/jupiterone-mcp picks up 2.5 on its next run. A global install updates with npm install -g @jupiterone/jupiterone-mcp.
The compliance tools need the Continuous Control Monitoring entitlement on your account. See Access and permissions.
What is new in MCP 2.5
| Feature | What it means for you |
|---|---|
Compliance summary (get-compliance-summary) | A scorecard of every CCM framework, with a drill-down into one framework, by id or name, for its sections and failing requirements, plus the attestations that expire soon and the expired ones failing controls now |
Controls list (list-controls) | The controls status view with its filters, including org units by id or display name, in compact rows with cursor paging |
Control details (get-control) | One control with its frameworks, requirements, control tests, latest test results, and attestations, plus the reason it passes or fails |
Attestations (create-attestation, update-attestation, revoke-attestation) | Record, renew, and revoke attestations on live controls. Each create and update reports what it does to the control's status, including when a failing test or an expired attestation keeps the control failing. A repeated create returns the existing attestation instead of a duplicate |
| Links to the JupiterOne app | Every framework and control in a compliance result carries a url to its page in the JupiterOne app, and your assistant gives that link when it names one |
| Control-test rule guard | The rule create, update, and delete tools refuse the rules CCM runs for its control tests, so an assistant cannot break a control test by editing its rule |
| Clearer access-denied messages | An access-denied error now leads with the reason JupiterOne gave, such as a missing entitlement, before the recovery guidance |
Compliance status through the MCP
The three read tools go from broad to narrow: the scorecard, the controls behind it, then one control in detail.
The compliance summary
get-compliance-summary returns every framework in the account with its control counts (total, passing, and failing), its requirement counts, and a health percentage. The attested count is every control with a valid attestation, passing or failing. It is not a subset of passing, because an attestation never masks a failing test. The health percentage is passing requirements divided by passing plus failing requirements. It is null when the framework has neither. It covers evaluated requirements only, so a framework can read 100% with 10 of its 130 requirements evaluated. Compare passing plus failing requirements with the total before calling a framework healthy.
The counts are read in time-boxed batches, so an account with many frameworks still gets a summary. statsIncomplete lists the frameworks whose counts did not return in time or could not be read. Their scorecards carry null counts and a null health percentage, which means not read, never zero. A drilled framework whose counts were not read reports sections: null, not an empty list. A call for one of them by frameworkId reads it.
Pass a frameworkId, or a frameworkName instead, to drill into one framework. The summary then reads and scores that framework only, adds the pass and fail counts for each section, and lists the failing requirements, critical ones first, each with the controls that fail it. A requirement counts as failing only when one of its live controls fails. Controls in Draft or Review keep whatever status they last had, so the summary ignores them, as CCM's own rollup does.
A frameworkName is matched as an exact id, then as an exact name ignoring case, then as the single framework whose name contains it. Framework names repeat, so several matches return ambiguous with up to 25 candidates, each with its id, name, source, sourceId, owner, import status, and url, plus a candidateCount. Same-named frameworks often differ only in their source, source id, or owner. An unknown framework, by id or by name, returns framework_not_found. Both results also list every framework in frameworks, with the same fields and no counts, so your assistant can choose from the full list. A drilled call reports how it matched in frameworkResolution. A partial match came from part of the name, so your assistant confirms the framework with you when it matters.
A drilled summary reads its failing controls, their requirements, and its no-data count from the JupiterOne graph, which CCM refreshes after each evaluation, and reports drillSource: "graph". When the graph cannot answer, it reads them from CCM itself and reports drillSource: "api". The scorecard counts, the health percentage, and the section counts are CCM's own either way. When the graph's failing controls and the scorecard's failing count differ, warnings says so.
A drilled summary adds two things:
failingControlslists every failing live control the summary read, with its id, identifier, name, andurl. On a large framework the requirement lookups can stop short, whichrequirementLookupsSkippedabove 0 orfailingControlsTruncatedreports.failingRequirementscan then be incomplete, butfailingControlsstill lists every failing control read. Your assistant answers from it, and reads a control withget-controlfor its requirements.requirementLookupsSkippedBycounts the skips by reason:timeBudget,cap, andreadFailed.capcovers controls past the first 50, and a control whose requirements ran past their own page limit, whose requirements read so far are kept. Calling again can recover time-budget skips, never cap skips.controls.passingWithoutDatacounts the live controls that pass on tests that saw no data. CCM counts them as passing, and so does the health percentage, so a high health figure can rest partly on tests that checked nothing. It isnullwhen they could not be counted, andpassingWithoutDataSkippedthen says why:time_budgetorread_failed. A control with no tests is not counted, because it passes on an attestation, not on tests that saw nothing.passingWithoutDataTruncatedistruewhen not every passing control was read. That happens past 500 controls, or past 1,000 test rows when the graph answers, because the graph returns one row per test. Your assistant lists those controls withlist-controls, using thatframeworkId,effectiveness: "NO_DATAPOINTS",status: ["PASS"], andstates: ["LIVE"].
The summary also returns two attestation lists:
expiringAttestationsholds the active attestations that expire withinattestationsExpiringWithinDays, which defaults to 30, soonest first.expiredAttestationsholds the attestations past their expiry that nobody revoked, most recently expired first. An expired attestation fails its control until it is renewed withupdate-attestationor revoked, whatever the tests say and even beside a valid attestation. These are the attestations failing controls right now. Each entry shows how many days ago it expired. The list holds at most 50, andexpiredAttestationsTruncatedistruewhen there were more.attestationsExpiringWithinDaysdoes not limit it.
One read names the controls in both lists and confirms which framework they belong to. It starts as soon as the request does, so names and scope are usually resolved. It covers at most 100 controls, the expired attestations' controls first.
- Scope.
expiringAttestationsScopesays what both lists cover. It readsframeworkwhen a drilled summary lists only that framework's controls. It readsaccountwithout a framework, when that read failed, or when more than 100 controls would need checking, and both lists are then account-wide. - Missing names.
controlNamesUnavailableistruewhen the read failed, ran out of time, or more than 100 controls were listed. Some control names are thennull, but eachcontrolIdis still exact. - Unconfirmed controls.
controlNamesUnresolvedcounts listed attestations whose control the read did not find. On a drill they stay listed even though their framework could not be confirmed, because a control created moments ago can be missing from the read. Your assistant confirms one withget-controlbefore reporting it as part of the framework.
Reading these lists across the account needs CCM read-admin. Without it each list reads unavailable and the rest of the summary still returns. unavailable means a list could not be read, not that nothing is expiring or expired.
A summary can also carry warnings, sentences your assistant passes on to you with the answer. Each says what the response could not confirm, such as attestation lists that stayed account-wide on a drill. There are no warnings when there is nothing to say.
Finding controls
list-controls is the controls status view as a tool. It filters by free-text search, status (pass or fail), lifecycle states, effectiveness (no tests or no datapoints), hasValidAttestation, owner, catalogs, mitreTechniques, frameworkId, source, and orgUnit. Each row carries the control's status, state, effectiveness, whether it is attested, its owner, its test counts, and the names of its frameworks. Results page with a cursor, 50 rows by default and up to 250. A cursor works only with the exact filters of the call that returned it. One the API refuses returns an error whose text starts cursor_rejected: and keeps the API's reason. The recovery is to call again with those filters and that cursor, or without a cursor for the first page. Sorting by LAST_EVALUATED_ON descending lists never-evaluated controls first; adding status: ["PASS", "FAIL"] leaves them out. When you name something that is not an account or a framework, your assistant treats it as a possible control and searches for it with list-controls before saying it does not exist.
For a count rather than rows, such as how many live controls fail, your assistant runs a J1QL count with execute-j1ql-query instead of paging through every row:
FIND ccm_control WITH state = 'LIVE' AND status = 'FAIL' AS c RETURN count(c) AS value
orgUnit selects the controls whose tests touch an asset in the org unit. Each row's status stays the control's overall status, not a status within the unit. The filter takes an org unit's display name or its id:
- A display name is looked up across the account's org unit namespaces. A name that matches more than one org unit returns
ambiguouswith the candidates to choose from instead of a guess. A name that matches none returnsorg_unit_not_found, withunitsoffering up to 10 org units from each namespace searched to choose from. - An id, written as
<namespace>:<uuid>, is used as given and reported asouResolution: "id_unverified". An empty result with that value may mean the id is mistyped rather than that the org unit has no controls.
Why a control passes or fails
get-control looks up one control by its controlId, its identifier, or its controlName. An identifier or name search considers every control that is not deleted, in any state. A search is found only when it read every candidate. That means a single match, or several matches of which exactly one is live. In the second case the live one is returned, because CCM evaluates only live controls, and otherCandidates lists up to 25 of the rest. Several matches without exactly one live control return ambiguous with up to 25 candidates and a candidateCount of all matches, instead of a guess. A search cut short also returns ambiguous, with searchComplete: false, even when it found a single match, because a second match may lie past the cut. Each candidate names its frameworks, which is usually how to tell them apart. A control that is not found comes back with a note that says to search with list-controls. The result carries the control's frameworks and requirements, its control tests with each test's latest evaluation, its attestations, and a statusExplanation. The tool checks the explanations in this order and reports the first that applies:
statusExplanation | Meaning |
|---|---|
not_live | The control is in Draft, Review, or Retired, so CCM does not evaluate it |
failing_test | At least one control test fails. An attestation never masks a failing test |
failing_expired_attestation | A non-revoked attestation has expired, which fails the control even when its tests pass |
undetermined | The stored status is a fail, but nothing the caller can read either fails or passes the control, or the control's attestations were unavailable |
passing_by_tests | At least one control test passes and none fails, including a test whose query saw no data. passingWithoutData: true flags that case, which is a pass on no data rather than evidence the control is met |
passing_by_attestation | No test has a status, and a valid attestation makes the control pass, which CCM shows as Attested |
no_tests | The control has no control tests and no valid attestation, so it is not evaluated |
no_datapoints | The control has tests, but none has a status yet, so it is not evaluated |
CCM scores a test by its status whenever the test has run, whether or not its query saw any data. A test whose query saw nothing still passes and counts. An attestation passes a control only when no test has a status, because a passing test already passes it. These follow the CCM 1.4 rules in How attestations affect control status.
statusExplanation names the first cause. A found control also carries blockers, which lists every cause that fails the control: each failing test and each expired attestation nobody revoked. It is empty when nothing fails the control. A control fixed for one cause keeps failing until every blocker is cleared. When the attestations are unavailable, blockers covers the tests only.
statusExplanation reads the current facts, while control.status is what CCM stored. CCM re-scores an attested control within 24 hours of an expiry and on its next test run, so the two can disagree for a while. When they do, storedStatusStale is true and expectedStatus gives the status the facts call for, PASS or FAIL. Your assistant reports that status and says CCM has not re-scored the control yet.
get-control answers within a fixed time budget. The control and its tests always come back, and for a lookup by controlId its attestations are read at the same time. Whatever else is not back in time is skipped rather than failing the call:
- Requirements come back empty, with
requirementsSkippedset totime_budgetorread_failedandrequirementsComplete: false. - Frameworks come back empty, with
frameworksSkipped. The control's ownframeworkIdsstill name them. - Attestations read as
unavailable, which inget-controlmeans they could not be read with the caller's permissions or within the time budget.
Explaining why a control fails needs only the control, its tests, and its attestations.
Each test's latest evaluation carries a rawDataKey. Passing it to get-rule-evaluation-query-results returns the assets that test scored, which is how your assistant answers "which assets make this control fail?". That tool takes an optional limit of up to 250 rows, because each row is a full entity. When rows are cut, the result carries truncated: true and the total before the cut. To see what the test's query matches right now instead, your assistant can run the test's own query with execute-j1ql-query. The tool looks up the latest evaluation for at most 10 rule-backed tests, failing tests first, and the lookups are time-boxed. A test that was not looked up is marked skipped rather than failing the call.
Links to the JupiterOne app
Every framework and control in a compliance result carries a url to its page in the JupiterOne app. That covers the scorecards, the drilled framework, its failing controls, the expiring and expired attestations (each links its control), the list-controls rows, get-control's control, its frameworks, and its candidates, and the control on every attestation write. When your assistant names a framework or control, it gives that link. The url is empty when no link can be built.
Attestations through the MCP
An attestation justifies a control with evidence that lives outside its automated tests, for a bounded time. The three attestation tools are write tools, annotated so your MCP client can ask for approval before it runs them.
Recording an attestation
create-attestation takes the control's controlId, a subject, and an expiresOn date. description (markdown), owner (an email address), and documentLink (an http or https URL) are optional. A documentLink that is not http or https is refused with a message that says so. Your assistant resolves the controlId with get-control first.
- Live controls only. Attestations apply to live controls, so the tool refuses a control in Draft, Review, or Retired. It reports the control's current state and writes nothing. See Control Lifecycle.
- The subject must not be blank. A subject of only whitespace is refused.
- The expiry must be in the future.
expiresOnaccepts a date asYYYY-MM-DD, read as the end of that day, 23:59:59.999 UTC, which is how the JupiterOne app stores it. Today's date is therefore accepted until the end of today. The same applies onupdate-attestation.expiresOnalso accepts a date-time withZor a±hh:mmoffset, or epoch milliseconds. A date-time without a time zone is rejected. So is a number below 1e12, such as epoch seconds or a compact date like20270630, rather than being read as a date in 1970. - A repeated create returns the existing attestation. Before creating, the tool checks the control's active attestations. If one has the same subject, ignoring case and surrounding spaces, the tool returns that attestation as
already_existsinstead of creating a second one. The check sees only what CCM can already read, so a second call made before the first write shows can still create a duplicate. If the check cannot read the control's attestations, the call fails rather than risk one. When several active attestations already share the subject,matcheslists them all, newest first. The returnedattestationis the one whose subject matches exactly, or the most recently created when none or several do. They are separate attestations, so checkmatchesbefore renewing one. Analready_existsresult also carriesrequestedExpiresOnIso, the expiry you asked for. When the existing attestation's expiry differs,requestedExpiresOnAppliedisfalseand the note says the requested expiry was not applied. Your assistant reports the existing expiry and changes it withupdate-attestation.
An unknown control and a control that is not live are both reported as errors, and nothing is written. Every compliance-tool refusal's text starts with its outcome, such as not_live:, not_found:, or invalid_input:. That is the same value structuredContent.outcome carries, for clients that show only the text. update-attestation and revoke-attestation handle an unknown attestation id differently. They return not_found as a normal result, with a note on how to recover: read the current ids from get-control, and attest again with create-attestation when the attestation was revoked.
What an attestation does to the control
An attestation passes a control only when no control test has a status, and it never masks a failing test. So created, already_exists, and updated results each carry a statusImpact and a blockers list:
statusImpact | Meaning |
|---|---|
blocked | Something keeps the control failing whatever is attested. A blocker is a failing test, or an expired attestation nobody revoked. On an update it can also be a control that is not live. Your assistant must not tell you the control is covered |
passes_by_attestation | No test has a status and nothing blocks it, so the control passes on its attestations once CCM re-scores it |
already_passing | A passing test already passes the control. The attestation is recorded but does not change the status |
unknown | On update-attestation only: the control could not be read back in time, or that read failed. The update itself went through and does not need repeating. The result's control then carries only its id and url, and there are no blockers |
The result's note names each blocker. A failing test is fixed on the control, and an expired attestation is renewed with update-attestation or revoked.
Expiry and renewal
When an attestation's expiry passes, the control fails, even over passing tests, until the attestation is renewed or revoked. CCM re-checks attested controls within 24 hours of an expiry. update-attestation renews an attestation by moving expiresOn later, before or after it lapses. An expired attestation given a future expiresOn is active again. It also edits the subject, description, owner, and document link, and it needs at least one field. Your assistant finds the attestationId in the attestations that get-control returns, or in the expiring and expired lists from get-compliance-summary.
Subjects should stay distinct on a control, because create-attestation finds attestations by subject. CCM allows two active attestations with the same subject. When an update leaves another active attestation on the control with the same subject, the result carries subjectCollision naming it, and the fix is to rename one of them.
Revoking an attestation
revoke-attestation is permanent. The attestation stops counting, and the control reverts to whatever its tests and any remaining valid attestations say. A revoked attestation cannot be edited or restored, though CCM's audit trail keeps its history. get-control usually drops it, but may still list it, often an older revocation, with state: "REVOKED". A revoked attestation never affects the status. Repeating a revoke is harmless: the second call returns not_found and changes nothing. To keep an attestation and extend it, use update-attestation instead.
A revoked result carries the control, with its url, and a statusImpact read after the revoke, with blockers:
statusImpact | Meaning |
|---|---|
no_longer_passing | Nothing else passes the control, so it is not evaluated once CCM re-scores it |
still_passing | A passing test or another valid attestation keeps the control passing |
still_failing | A failing test or another expired attestation keeps the control failing. blockers lists them |
now_passing | The revoked attestation had expired, so revoking it cleared a failure, and something else passes the control |
no_longer_failing | The revoked attestation had expired, and nothing else passes the control, so it is not evaluated |
unknown | The control could not be read back in time, or that read failed. The revoke went through and does not need repeating |
Reads catch up after a write
CCM's reads trail its writes by a few seconds, occasionally up to a minute. Straight after create-attestation succeeds, get-control may not list the new attestation and the scorecard may not count it yet. After an update or a revoke, get-control can still show the old values for a while, or the attestation as active. The write's own response is the confirmation. Re-read after a pause rather than repeating the write.
When a write is interrupted
A write that went through is never reported as an error. One error, interrupted, means the request reached the server's time limit while the write was being made, so it may have landed. Its text starts interrupted:, and the recovery depends on the tool:
create-attestation: do not call it again at once. Read the control withget-controlfirst. Its attestations list the subject once CCM can read it, which can take up to a minute, so wait a minute if it is not there yet before deciding whether another create is needed. Inside that minute the duplicate check cannot see the first attestation either, so an immediate retry can create a duplicate.update-attestation: read the attestation withget-controlbefore repeating the update.revoke-attestation: call again.not_foundmeans the first call revoked it.
Rules that CCM runs for control tests
CCM runs each control test as a rule in the account's rule engine, named CONTROL-TEST:<test name>. CCM creates and reconciles these rules itself, so editing or deleting one directly breaks its control test. They are internal rules, so list-rules never lists them. Asked for one by id, get-rule-details reports it as an internal rule that is read through get-control on its control.
From 2.5 the rule write tools refuse them:
- By name.
create-inline-question-ruleandupdate-inline-question-rulerefuse a rule name that startsCONTROL-TEST:. - By reference. Before acting,
update-inline-question-ruleanddelete-rulecheck the graph for a control test that references the rule id, and refuse when one exists. This covers any change, including the rule's polling interval. - When the check cannot finish.
delete-rulerefuses and deletes nothing, and a retry is safe. An update goes ahead and reportsownershipCheck: "skipped".
To change what a control test checks, change the test on its control, not its rule.
Clearer access-denied messages
When JupiterOne denies a call for a specific reason, the error now leads with that reason. A call from an account without CCM, for example, returns a message that begins:
Not authorized (HTTP 403): Feature access not allowed to CCM.
A recovery sentence follows, then the tool's own hint. For a missing permission or entitlement, the recovery sentence says that re-authenticating will not help and that an account admin can grant access. Before 2.5 the server discarded the reason and returned only general guidance about permissions and account membership. A denial that gives no specific reason keeps that general guidance. The change applies to every tool, not only the compliance tools.
Other changes
list-rule-evaluations now returns at most limit runs and stops reading pages once it has them. Before 2.5 it read every page and returned every run whatever the limit. Without a limit it still reads every page.
Access and permissions
The compliance tools are available on every connection, and JupiterOne checks access on each call.
- All six tools need the Continuous Control Monitoring entitlement on the account and control read permission.
create-attestation,update-attestation, andrevoke-attestationalso need edit permission on the control.- The expiring and expired attestation lists in
get-compliance-summaryneed CCM read-admin, because they read attestations across the whole account. Without it each list readsunavailableand the rest of the summary still returns.
Getting started
Remote server: nothing to do. Your connector already runs 2.5.
Local server: restart your AI client. npx picks up the new version automatically.
Verify the upgrade. Ask your assistant:
"Test my JupiterOne connection"
test-connection reports the package version alongside your identity and permissions. Then try:
"Give me a compliance summary of our frameworks"
More compliance prompts are in the Compliance (CCM) tab of the prompt templates, and every tool is listed under Available Tools. For CCM itself, see Continuous Control Monitoring and What's new in CCM 1.4. For the 2.x line, see What's new in MCP 2.0.