SARIF explained: why linters speak it, and how mcpscore 1.14.0 writes it
· 7 min read
A finding that lives in a CI log gets read once, by whoever opened the log. A finding in your repository’s Security tab gets an owner, a history, and a close date. SARIF is the format that moves findings from the first place to the second, and mcpscore 1.14.0 writes it. This post explains what SARIF is, why a linter should speak it, and exactly what mcpscore puts in the file.
What SARIF is
SARIF, the Static Analysis Results Format, is an OASIS standard: a JSON document that describes what an analysis tool found. Version 2.1.0 is the one everything reads. The shape is small. A file holds one or more runs. A run names the tool that produced it and the rules it applied, then lists results. Each result points at a rule, carries a level of error, warning, or note, a message, one or more locations, and a fingerprint that identifies the finding across runs.
That is enough for very different tools to agree on. CodeQL emits it. ESLint and Semgrep have SARIF formatters. Trivy writes it for container images. On the other side, GitHub code scanning, Azure DevOps, and the SARIF viewer for VS Code all read it. A tool that speaks SARIF does not need to build a dashboard, and a platform that reads SARIF does not need to know the tool.
Why a linter should speak it
A linter’s job is to find things. Tracking those things is a different job, and it is the one teams struggle with. A red CI job says something is wrong today. It does not say when the problem appeared, whether anyone looked at it, or that it went away three commits later. GitHub’s Security tab does all of that, but only for findings that arrive as SARIF.
Three properties make the format worth the effort. First, findings get a lifecycle: an alert opens when a result first appears, stays open while later uploads keep reporting it, and closes as fixed when an upload no longer does. Second, findings de-duplicate. The fingerprint tells the platform that this week’s result is the same finding as last week’s, so a re-run updates one alert instead of filing a second. Third, findings land next to everyone else’s. A security engineer who already triages CodeQL and dependency alerts in one tab gets MCP server findings in the same tab, with the same filters, the same severity bands, and the same dismiss reasons.
For a tool like mcpscore there is a twist. Our findings are about a running server, not a line of source. SARIF still fits, because GitHub accepts a repository-relative path as a location even when no file sits there, and the alert view shows the rule, the message, and the help link exactly as it would for a code finding. What you do not get is an annotation in the pull request diff, since there is no changed line to annotate. The job’s exit code is what fails the pull request; the Security tab is where the finding lives afterwards.
How mcpscore writes it
One flag. --sarif FILE writes the failed rules of an audit as SARIF 2.1.0, for a server or a package audit alike. Pass - to write to stdout instead, or keep --json on stdout and send the SARIF to a file, which is the CI shape.
# Findings to a file for upload; the JSON report still goes to stdout
uvx mcpscore==1.14.0 'https://your-server.example/mcp' --json --sarif mcpscore.sarif
# Audit finished. Final score: 78/91
# SARIF written to mcpscore.sarif (12 findings)The file is written before any gate runs, so a build that fails --fail-under still carries its findings to the upload step. It contains findings only: one result per failed rule, keyed by the same rule_id the JSON report and the rules reference use. Passed and skipped rules are not in it, because SARIF describes what is wrong and the JSON report remains the complete record of the run.
{
"ruleId": "protocol_version_latest",
"level": "warning",
"message": {
"text": "❌ Not using the latest protocol version: negotiated '2025-11-25' …"
},
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": "mcp.deepwiki.com/mcp" }
}
}],
"partialFingerprints": {
"primaryLocationLineHash": "f1dcb75c967781b90dfa2a3e693e639b"
},
"properties": { "severity": "MEDIUM", "counted_in_score": true }
}The level follows the rule’s severity: CRITICAL and HIGH become error, MEDIUM becomes warning, LOW becomes note. One deliberate exception: a readiness rule that this run did not count in the score becomes a note whatever its severity, because the Security tab must not show as an error something the score itself waved through. Security & Auth rules also carry GitHub’s security-severity, so they sort into the tab’s critical, high, medium, and low bands with the other security findings.
The fingerprint sits under primaryLocationLineHash, the one key GitHub matches alerts on, and hashes the rule id with the target’s identity. Audit the same server next week and the alert updates; audit a second server and its findings are a second set. The location is a repository-relative path standing for the target, such as mcp.deepwiki.com/mcp, because GitHub rejects an upload whose locations use any other URI scheme.
What the file shows of your server
A code scanning upload is readable by everyone who can read the repository’s alerts, a wider audience than a terminal. So the file shows a fixed set of things about the target and nothing else: a URL’s scheme, host, port, and path; a --stdio command’s program name; a local path; a package coordinate. Never a URL’s userinfo, query, or fragment, never a command’s arguments, and never a rule’s details, which can hold the audited URL verbatim. URLs that a rule message quotes, such as an authorization server’s issuer, are cut down the same way, and the alert identity is hashed from that shown form, so nothing the file hides enters a hash either.
That is the whole rule, and there is no heuristic behind it. A parser that tries to guess which part of a path or a command line is a secret can always be shown one more input it missed. Keep credentials in --token, --header, or --env NAME, as the CLI has always advised, and the file holds nothing you would not paste into an issue.
In the GitHub Action
The mcpscore GitHub Action v1.2.0 adds a sarif-path input. Set it, add the standard upload step, and every failed rule becomes an alert.
jobs:
audit:
runs-on: ubuntu-latest
permissions:
pull-requests: write
security-events: write # lets upload-sarif create the alerts
contents: read # needed in a private repository
steps:
- uses: mcp-box/mcpscore-action@v1
with:
target: https://your-server.example/mcp
min-score: 80
sarif-path: mcpscore.sarif
- uses: github/codeql-action/upload-sarif@v4
if: ${{ !cancelled() && hashFiles('mcpscore.sarif') != '' }}
with:
sarif_file: mcpscore.sarifThe upload needs security-events: write, and a private repository needs contents: read as well. The if condition uploads after a failed gate, which still wrote the file, and skips after a connection failure, which did not. The upload action files alerts under a category per workflow and job, and allows one upload per job for a given category; to audit two servers in one job, give each upload step its own category input.
Try mcpscore 1.14.0
Read the complete release notes, inspect the package on PyPI, and see the full mapping and file shape in the CLI reference and the upload workflow in the GitHub Action guide. SARIF composes with the rest of the CLI: a configured run exports the findings under your own policy, and the other MCP testing tools keep their own place next to it in the same tab.
The canonical score is always one paste away on mcpscore.dev. The findings behind it now have a place to live.