Content
# c2pa-mcp
**An MCP server that verifies C2PA Content Credentials and returns an LLM-ready verdict.**
Point any MCP client (Claude Desktop, Claude Code, Cursor, ...) at a local file or a URL and get back a plain-language answer: is this image/video/audio **trusted, valid, tampered, or unsigned**? Who signed it? Is it **AI-generated**? What's its edit history and provenance lineage?
Built by [c2paviewer.com](https://c2paviewer.com). Verification runs locally on the [official C2PA Rust engine](https://github.com/contentauth/c2pa-rs) via `@contentauth/c2pa-node`. Files never leave your machine.
> **Read-only.** This server verifies and inspects Content Credentials. It does not sign or create them.
## Install
No global install needed. Add it to your MCP client config and it runs via `npx`:
```json
{
"mcpServers": {
"c2pa": {
"command": "npx",
"args": ["-y", "@c2paviewer/c2pa-mcp"]
}
}
}
```
- **Claude Desktop**: Settings → Developer → Edit Config, add the block above.
- **Claude Code**: `claude mcp add c2pa -- npx -y @c2paviewer/c2pa-mcp`
- **Cursor / others**: add the same `mcpServers` entry to the client's MCP config.
Requires Node.js 18+.
## Tools
| Tool | What it does |
|------|--------------|
| `verify_c2pa_file` | Verify a local image/video/audio/PDF by path. |
| `verify_c2pa_url` | Download a public https media URL and verify it (SSRF-guarded). |
| `scan_c2pa_directory` | Audit a folder: which files have credentials, their verdict, signer, AI status. |
| `c2pa_info` | Report engine version, supported media types, and trust-list status. |
Each verify tool returns a human-readable summary plus a structured digest:
```jsonc
{
"verdict": "invalid", // trusted | valid_untrusted | valid_trust_unknown | invalid | no_credentials
"summary": "Content Credentials are INVALID: an integrity or signature check failed ...",
"signer": { "name": "Example Signer", "trusted": false },
"aiGenerated": { "isAI": true, "tools": ["DALL-E"], "digitalSourceTypes": ["...trainedAlgorithmicMedia"] },
"provenance": [ { "depth": 0, "title": "This file", "relationship": "This file", "verdict": "invalid" } ],
"edits": [ { "label": "Created", "agent": "Photoshop", "when": "...", "detail": "" } ],
"watermarks": [ { "kind": "synthid", "assertionLabel": "...", "algorithm": "" } ],
"issues": [ { "code": "assertion.dataHash.mismatch", "severity": "error",
"explanation": "The media content was changed after it was signed. ..." } ],
"trust": { "evaluated": true, "listSource": "https://.../C2PA-TRUST-LIST.pem" }
}
```
Pass `"includeRaw": true` to also get the full raw manifest store.
## Trust list
To report a signer as **`trusted`** (not just cryptographically valid), the server checks the signing certificate against two trust lists, **fetched live and cached** (24h TTL) so decisions stay current without a release:
- the official [C2PA Conformance Program list](https://github.com/c2pa-org/conformance-public) (going-forward signers), and
- the legacy [CAI Interim Trust List](https://verify.contentauthenticity.org/trust/anchors.pem) (frozen Jan 2026, but still the only list that recognizes pre-conformance signers — Adobe, Leica, Truepic, Canon, Samsung, and most real-world content today).
Both are on by default; without the ITL, mainstream signed content would read as `valid_untrusted`.
If the trust lists can't be fetched, the server **degrades loudly**: verification still runs, but the verdict becomes `valid_trust_unknown` and `trust.evaluated` is `false` with a reason. It never silently treats an unknown signer as trusted, and never silently uses a stale snapshot.
Environment overrides:
| Variable | Default | Purpose |
|----------|---------|---------|
| `C2PA_TRUST_LIST_URL` | conformance + ITL | Comma-separated PEM URLs. Replaces the defaults (conformance list + Interim Trust List). |
| `C2PA_TRUST_TTL_SECONDS` | `86400` | Cache lifetime for the fetched trust list. |
| `C2PA_MAX_FETCH_BYTES` | `104857600` | Max download size for `verify_c2pa_url` (100 MB). |
| `C2PA_MAX_FILE_BYTES` | `524288000` | Max size of a local file the file/scan tools will verify (500 MB). |
| `C2PA_ALLOWED_ROOTS` | (unset) | Comma/semicolon-separated absolute paths. When set, `verify_c2pa_file` and `scan_c2pa_directory` refuse any path outside these roots. |
## Security
- **Local processing.** Files are read and verified on your machine; nothing is uploaded.
- **SSRF-guarded URL fetching.** `verify_c2pa_url` allows only public `https`, pins the resolved IP against DNS rebinding, re-validates every redirect, sends no cookies or auth, and enforces a content-type allowlist plus size cap.
- **Local path safety.** The file/scan tools reject remote `file://` and UNC paths, resolve symlinks before the allowed-roots check, and return a generic error for inaccessible paths.
- **Set `C2PA_ALLOWED_ROOTS` in any untrusted context.** The file/scan tools expose the host filesystem by design, so a prompt-injected model could point them anywhere. Confining them to a specific directory is the single most important hardening setting.
- **Trust integrity.** Trust lists are fetched over HTTPS (integrity rests on TLS and the upstream sources); the cache is per-user (`0700`/`0600`, ownership-checked) and bound to the configured URL set.
## Limitations
- **Experimental. Not legal evidence.** C2PA tooling and trust infrastructure are still evolving. Do not rely on these verdicts for legal, compliance, or safety-critical decisions.
- **Watermarks are reported as declared, not pixel-verified.** A `synthid` entry means the manifest *declares* a SynthID watermark; confirming the signal in the pixels requires the vendor's detector.
- **AI-generation reflects what the manifest declares** via IPTC `digitalSourceType`. Absence of an AI declaration is not proof the content is not AI-generated.
## Development
```bash
npm install
npm run build
npm test # builds, then runs unit + end-to-end tests (network needed for the trust list)
```
## License
Source code is dual-licensed under [MIT](./LICENSE-MIT) or [Apache-2.0](./LICENSE-APACHE), at your option.
The test images under [`test/fixtures/`](./test/fixtures/) are redistributed unmodified from [`c2pa-org/public-testfiles`](https://github.com/c2pa-org/public-testfiles) and are licensed separately under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
> Note: if you publish under an unscoped name instead of `@c2paviewer/c2pa-mcp`, change `name` in `package.json` and the `args` in the install block above; nothing else depends on the package name.