CDOP MCP Server
OfficialRenders the /atlas visual explorer: every project and registry account plotted on a map, with drill-throughs into project status history, issuances, units and CDOP documents. Requires a public Mapbox API key (MAPBOX_API_KEY); without it the rest of the explorer (charts, drill-throughs) still works.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@CDOP MCP Serversearch for projects in Kenya and validate their CDOP documents"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
CDOP reference API
A reference implementation of an API for the Carbon Data Open Protocol (CDOP) schema v2.0.
It serves the CDOP documents over a hypermedia REST API (HAL + HAL-FORMS). It publishes an OpenAPI 3.1 description that embeds the CDOP JSON Schemas verbatim. It exposes the same data to AI agents over MCP and streams changes as CloudEvents. It ships with a synthetic multi-registry dataset and a lifecycle simulator. Every place where the schema could not be implemented as published is logged in SCHEMA-FEEDBACK.md.
Maintained by Rethink Carbon. MIT licence.
Why this exists
CDOP is a member-driven data standard for carbon credit projects (co-chairs GCMU, Sylvera, RMI and S&P Global; 75 members). Schema v2.0 was published on 16 to 18 September 2026. It is authored in Excel and converted to JSON Schema 2020-12 by a script. The published artefacts are twelve schema files and eleven example documents. There is no API, the examples do not validate against the schemas, there is no lint step, and there is no model for change or synchronisation.
In September 2026 the CDOP Technical Working Group confirmed it wanted a reference implementation of an API. Rethink Carbon committed to deliver one. The commitment covers a working API with an OpenAPI description and synthetic data for every current schema document. It also covers an exploration of hypermedia navigation, and a written record of every gap, ambiguity or decision that could affect schema development. This repository is that deliverable. Rethink Carbon also uses the protocol itself.
Related MCP server: RationalBloks MCP Server
Status
M1 (v0.1) is feature complete locally: the read-only API, the seed, the conformance tests and the MCP read surface all pass CI's checks. It is not yet deployed to the hosted demo. The endpoint table below marks each route with the milestone in which it lands.
Milestone | Scope | Target |
M1 | Read-only API, all CDOP documents, OpenAPI, Scalar API reference, hal-explorer, MCP read tools, seed of 370 projects across seven standards, schema-feedback register | 27 September 2026 |
M2 | Ledger (issuances, unit blocks), HAL-FORMS actions with API keys, events (SSE, pull feed, webhooks), simulator, accounts, MCP write tools | weeks 2 to 3 |
M3 | 500 projects across 7 standards, golden fixtures, rate limiting, demo reset, | weeks 4 to 5 |
M4 | TWG iteration, Round 2 pods (Registry, Validation, Verification Metadata), | week 6 |
Quickstart
You need Docker with Compose. Nothing else.
git clone https://github.com/rethink-carbon/cdop-reference-api.git
cd cdop-reference-api
docker compose upThe first boot applies the migrations and seeds the synthetic dataset (SEED_ON_START=true). Then open:
URL | What |
Visual explorer: map, charts and drill-throughs (see below) | |
OpenAPI 3.1 reference (Scalar) | |
HAL browser (hal-explorer) | |
HAL root: follow the links from here | |
MCP endpoint (Streamable HTTP) | |
Health check |
/atlas shows every project and registry account on a map, with drill-throughs into a project's status history, issuances, units, documents and CDOP documents, and charts of projects by stage and units by vintage. It is a HAL client of this API and lists the calls behind each view. The map needs a public Mapbox token: MAPBOX_API_KEY=pk.… docker compose up. Without one, everything but the map works.
If ports 3000 or 5433 are busy on your machine, set CDOP_PORT and CDOP_DB_PORT before starting:
CDOP_PORT=3100 CDOP_DB_PORT=5434 docker compose upThe hosted demo runs at https://cdop.rethinkcarbon.co.uk with the same routes.
A five-step tour with curl
The API is hypermedia. Start at the root and follow links; you never need to construct a URL. Each response carries _links, and the cdop curie resolves every custom relation to a page under /rels/.
Step 1: the root.
curl -s http://localhost:3000/v2 | jq '._links | keys'Step 2: follow cdop:projects to the collection. Collections are compact index rows under _embedded, keyed by the collection's name (projects, units, documents), with total, limit and next/prev/first links. Filters use CDOP field names.
PROJECTS=$(curl -s http://localhost:3000/v2 | jq -r '._links["cdop:projects"].href')
curl -s "$PROJECTS?standard=wcc&limit=3" | jq '{total, limit, items: [._embedded.projects[] | {id, project_name, lifecycle_stage}]}'Step 3: open one project. The resource uses CDOP field names verbatim, plus status (the current status record), lifecycle_stage and registry_status.
PROJECT=$(curl -s "$PROJECTS?standard=wcc&limit=1" | jq -r '._embedded.projects[0]._links.self.href')
curl -s "$PROJECT" | jq '{project_name, lifecycle_stage, registry_status, status, documents: [._links["cdop:document"][].name]}'Step 4: fetch the schema-pure Full List document. cdop:document is an array of named links, one per CDOP pod. Note the ETag, the Link: rel="describedby" header and X-CDOP-Schema-Version.
FULL_LIST=$(curl -s "$PROJECT" | jq -r '._links["cdop:document"][] | select(.name == "full-list") | .href')
curl -s -D - "$FULL_LIST" -o full-list.json | grep -iE '^(etag|link|x-cdop)'Step 5: validate it. POST /v2/validate runs Ajv (JSON Schema 2020-12) against the vendored CDOP schema.
curl -s -X POST "http://localhost:3000/v2/validate?schema=full-list" \
-H 'content-type: application/json' --data @full-list.json | jq '{valid, errors: (.errors | length)}'Expect "valid": false. That is the point of the exercise, not a bug: no Full List document for a voluntary-market project can validate against the published schema, because project.compliance_market_id is required with at least one item (CDOP-FB-020). The X-CDOP-Conformance header from step 4 names the register entry behind every error, for example invalid; errors=4; ref=CDOP-FB-005,CDOP-FB-020,CDOP-FB-028. The pods those defects do not touch validate. Add ?strict=1 to have the API fill required-but-unpublished sections with flagged placeholders:
ESTIMATIONS=$(curl -s "$PROJECT" | jq -r '._links["cdop:document"][] | select(.name == "estimations") | .href')
curl -s "$ESTIMATIONS?strict=1" | curl -s -X POST "http://localhost:3000/v2/validate?schema=estimations" \
-H 'content-type: application/json' --data @- | jq '{valid}'A longer walkthrough, including If-None-Match, the URN resolver, HAL-FORMS templates and problem details, is in docs/hateoas-tour.md.
Three surfaces, one model
REST (HAL + HAL-FORMS). Every resource carries _links. Collections embed compact rows. State-gated actions appear as _templates (M2), derived from one transition table per entity, so a retired block never offers retire. Errors are RFC 9457 application/problem+json with a documented type per problem (/problems/{slug}). Custom relations are documented under /rels/{rel} and in docs/rels/.
OpenAPI 3.1. /v2/openapi.json describes our routes and embeds each CDOP schema under components.schemas["cdop.v2.<Pod>"] without re-expressing it. Scalar renders it at /docs, served from this origin (see "Self-hosted UIs and optional external services" below). The artefact is committed at apps/api/openapi/openapi.json and CI fails if it drifts from the code.
MCP. /mcp mounts an MCP server over Streamable HTTP with the same services behind it: search_projects, get_project, get_cdop_document (with an Ajv conformance report), validate_payload, explain_schema, list_enum, get_state_machine and more. Results carry HAL links so an agent can keep navigating. See docs/mcp.md.
Events (M2). Every status-record append becomes a CloudEvents 1.0 event of type org.cdop.<entity>.status.changed, written to one outbox table. Consume them over SSE (/v2/events, resumable with Last-Event-ID), as a pull feed (/v2/changes?since=), or as signed webhooks (/v2/webhooks). A row edited directly in the database produces org.cdop.row.changed.
Conformance
The CDOP schemas are vendored, not re-expressed. packages/cdop-schemas/schemas/v2/ holds the twelve schema files and examples/v2/ the eleven upstream examples, pinned to upstream commit eff6ca3 (16 September 2026) with a SHA-256 per file in UPSTREAM.json. Ajv 2020-12 validates every CDOP-shaped payload, with the x-cdop-* annotation keywords registered as a vocabulary.
Command | What it does |
| Recomputes every vendored file's hash against |
| Static checks the upstream pipeline lacks: keys with spaces, mangled keys, integer identifiers, links and dates without |
| Re-vendors from upstream at a commit and rewrites |
| Compares upstream |
The test suite asserts that the upstream examples for Unit Description, Estimations, Crediting Period and Full List do not validate against the upstream schemas (see CDOP-FB-004).
It also projects every seeded project through every pod and validates the result. Eight of the eleven project pods validate for every project under every standard. The other three fail only for reasons the register records, and the suite fails the build on any error no register entry explains:
Pod | Result | Why |
Location Details, Disclosures, Issuances, Crediting Period, Estimations, Co-Benefits, Durability & Permanence, Project Finance | valid for every project | |
Project Approach & Details | valid except WCC and Peatland Code projects | The UK codes are missing from the program and methodology enums ( |
Full List, Labels & Certifications | invalid for every project |
|
API version is not schema version
The URL prefix /v2 is the API major version. The CDOP schema version travels separately in a response header on every CDOP document and on the root:
X-CDOP-Schema-Version: 2.0+eff6ca3
X-API-Version: 0.1.0The schema label is derived from UPSTREAM.json, so re-vendoring the schema changes the header without touching the API version. A ?strict=1 query on a cdop/{pod} document adds X-CDOP-Conformance, which explains any placeholder the API had to emit to satisfy a defect in the published schema.
Self-hosted UIs and optional external services
The running service talks to its Postgres database and nothing else. /docs and /explorer are served from the API's own origin and the browser is told to enforce that. Mapbox and operator-configured Rybbit analytics are the scoped exceptions described below.
/docsis Scalar (MIT): the browser bundle from@scalar/api-reference, pinned to an exact version in the lockfile and inpnpm audit, copied alone into the image. No CDN. Its font CDN, telemetry, hosted AI agent and hosted-client link are switched off, and its "Connect MCP" entry points at this API's own/mcp(ADR 0009)./exploreris HAL Explorer (MIT), vendored. Its theme picker is hard-wired tobootswatch.com; the API serves the themes itself instead, so every theme is the Bootstrap build already in the bundle.By default both carry
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; .... A script, stylesheet, font or request to any other origin is blocked by the browser, not just absent by convention. The test suite fails if either page references another origin.Install scripts run only where
pnpm-workspace.yamlallows them (vue-demi, pulled in by Scalar, is denied), and the image installs with--ignore-scripts./atlasdraws its basemap with Mapbox, which cannot be self-hosted (ADR 0010). Only whenMAPBOX_API_KEYholds a public token does the page load Mapbox GL JS fromapi.mapbox.com, at an exact version with a Subresource Integrity hash, and only that page's policy allowsapi.mapbox.com,*.tiles.mapbox.comandevents.mapbox.com. Mapbox then sees each visitor's IP address, this origin as referrer, and their map loads. Without a token the page keeps the self-only policy. Its own scripts and charts are served from this origin, and the image contains no Mapbox code.Optional Rybbit analytics (ADR 0011): set both
RYBBIT_ORIGIN(HTTPS origin, no trailing slash) andRYBBIT_SITE_IDto count visitors to/docs/,/explorer/and/atlas/, plus Atlas navigation and actions. Only the configured origin is added toconnect-src; scripts stay local. Search text, URL queries, API credentials and session replay are excluded. Do Not Track and Global Privacy Control are respected. Unset both to disable it.
Local development
Prerequisites: Node 24 (.nvmrc), pnpm 12 via corepack (corepack enable), Docker.
pnpm install --frozen-lockfile
docker compose up -d db # Postgres 17 on localhost:5433
cp .env.example apps/api/.env # the API scripts read .env from apps/api/
pnpm db:migrate # applies supabase/migrations/*.sql with checksums
pnpm seed # deterministic synthetic dataset (CDOP_SEED=2026)
pnpm dev # tsx watch on http://localhost:3000
pnpm testpnpm seed -- --counts wcc=8,pc=6,vcs=6 seeds a smaller mix (this is what CI does). The Supabase CLI works too: supabase start runs the same migrations on a local stack (API 54421, database 54422, Studio 54423). pnpm db:migrate refuses to run against a database the Supabase CLI already manages, to avoid applying files twice.
Environment variables are documented in .env.example. The API needs only DATABASE_URL.
Endpoints
All API routes live under /v2, respond with application/hal+json, and use RFC 9457 problem details for errors.
Method | Path | Notes | Milestone |
GET |
| HAL root: curies, collections, | M1 |
GET |
| Filters use CDOP field names ( | M1 |
GET |
| Status records newest first, with | M1 |
GET |
|
| M1 |
GET |
|
| M1 |
GET |
|
| M2 |
GET |
| Schema-pure CDOP document: | M1 |
GET |
| One resource is one credit block; | M1 |
GET |
| M1 | |
GET |
| Registry accounts | M1 |
GET |
| Resolves a CDOP URN with a 303 | M1 |
GET |
| Every enum in the vendored schema | M1 |
GET |
| The vendored schema files, served intact | M1 |
GET |
| Transition tables and lifecycle mapping tables | M1 |
POST |
| Ajv result for any payload | M1 |
POST |
| Transitions from HAL-FORMS templates; | M2 |
POST |
| Create from a CDOP Project Approach & Details document | M2 |
PUT |
| Upsert a pod document | M3 |
GET |
| SSE stream; | M2 |
GET |
| Pull feed of CloudEvents; | M2 |
GET, POST, DELETE |
| Standard Webhooks signatures, 8 retries over about 8 hours | M2 |
ALL |
| MCP Streamable HTTP, stateless | M1 read, M2 write |
GET |
| M1 | |
POST |
| Reseed; simulator start, stop and rate (admin key) | M3 |
Reads are anonymous. Writes need Authorization: Bearer cdop_<keyid>.<64hex>. Keys are stored as SHA-256 hashes with one of the roles developer, vvb, code_admin, registry, admin, sandbox. pnpm seed prints demo keys for a local database. pnpm keys:new --role <role> --label <text> stores a new key and prints its token once; pnpm keys:new on its own prints a token for ADMIN_API_KEY, which is never stored.
Repository layout
apps/api/ the API, MCP server, seeder and simulator (Hono, Kysely, Ajv)
src/domain/lifecycle/ per-standard state machines and the canonical vocabularies
src/http/ HAL, HAL-FORMS, problem details, cursors, ETags
src/db/ Kysely setup and the SQL migration runner
src/seed/ deterministic synthetic data generator
src/routes/ src/mcp/ read-only routes and the MCP server (M1)
src/sim/ src/webhooks/ land in M2
test/ generator, conformance and HTTP suites
packages/cdop-schemas/ vendored CDOP schemas + examples, field registry, Ajv validator, lint, sync, drift
supabase/migrations/ SQL migrations (run by docker compose, supabase start and supabase db push alike)
supabase/config.toml local Supabase stack configuration
docs/ ADRs, link relations, API profile, tours, deployment
docker-compose.yml API + Postgres 17 for forkers
docker-compose.dokploy.yml API only, for the hosted demo behind Traefik
.github/workflows/ ci, docker (GHCR + Dokploy webhook), schema-driftDocumentation
SCHEMA-FEEDBACK.md: the numbered register of schema defects, ambiguities and API gaps found while implementing.
docs/api-profile.md: the companion "CDOP API profile" proposal (identifiers, timestamps, pagination, events, lifecycle vocabularies).
docs/hateoas-tour.md: the long curl walkthrough.
docs/projection.md: how
cdop.*tables map onto CDOP document sections.docs/mcp.md: connecting Claude Code, Claude Desktop and other MCP clients.
docs/deploy-dokploy.md: the hosted deployment runbook.
docs/adr/: architecture decision records.
docs/rels/: one page per custom link relation.
CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, CHANGELOG.md.
Data
All data in this repository and on the hosted demo is synthetic. Names, places, identifiers, dates and figures are generated deterministically from a seed. Nothing is taken from a real registry.
Licence
MIT. See LICENSE. The CDOP schemas are vendored under their own MIT licence from the upstream repository.
This server cannot be deployed
Maintenance
Related MCP Connectors
Versioned documentation registry and semantic search for AI tools and coding assistants.
Build, validate, deploy — HTTP APIs, cron jobs, webhooks and MCP tools — from your AI client.
Serves your design system and coding standards to coding agents, so they stop guessing.
Public social-data API and live docs for AI coding agents.
Related MCP Servers
- AlicenseAqualityBmaintenanceEnables AI agents to lint API artifacts (OpenAPI, AsyncAPI, Arazzo) and manage rulesets conversationally via the Model Context Protocol.4Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to deploy production APIs from JSON schemas, with 44 tools for managing projects, schemas, deployments, and graph data.1Academic Free v1.1
- AlicenseNot gradedqualityCmaintenanceProvides AI coding agents with accurate OpenAPI contract details to prevent hallucinated API calls, supporting multi-version pinning, endpoint discovery, and request validation.45 npmApache 2.0
- FlicenseAqualityBmaintenanceEnables AI agents to interact with issue, comment, document, and agent workflows through a simplified HTTP API, returning compact Markdown or file-based snapshots.16-