Skip to content

fix(schema): publish UCP annotated schemas#125

Merged
igrigorik merged 11 commits intomainfrom
feat/ucp-schema
Jan 28, 2026
Merged

fix(schema): publish UCP annotated schemas#125
igrigorik merged 11 commits intomainfrom
feat/ucp-schema

Conversation

@igrigorik
Copy link
Contributor

Our contract is that UCP schemas are self-describing. We define and advertise one URL in capability metadata, which agents and servers can fetch and validate against. At the moment, this contract is broken.

  "dev.ucp.shopping.fulfillment": [
    {
      "version": "2026-01-11",
      "spec": "https://ucp.dev/specification/fulfillment",
      "schema": "https://ucp.dev/schemas/shopping/fulfillment.json",  // <~ single schema
      "extends": "dev.ucp.shopping.checkout"
    }
  ]

A schema yields different shapes based on operation (e.g., create vs update vs complete) and direction (request vs response). We encode these rules via ucp_request/ucp_response annotations in our source/* schemas, but then run a preprocessor that splits them into N permutations based on operation × direction (e.g., checkout.create_req.json maps to create + request). The schema URL we advertise in UCP metadata doesn't contain all the necessary information, and we've defined a convention that off-the-shelf schema validators are not aware of and would need to be taught to understand. As is, this makes UCP validation very hard to implement.

Proposed desired state

Deliver self-describing schemas. Keep servers+agents simple: one URL. Handle complexity in shared tooling.

By count and burden: servers > agents > validators. If we require that servers dynamically resolve and return operation-specific schema refs, that's complexity multiplied across every UCP business / implementer. Same for agents, who would need to vary their profile per operation — clunky. We can eliminate and shift all of this complexity into validators: restore the single file contract, a ref to which can be passed in both directions by servers+agents, and lean on UCP-aware tooling.

Actor Per-op files (today) Single schema (proposed)
Spec authors N files per capability (via preprocessor) 1 file
Server implementers Hardcode operation→URL mapping Return one URL
Validator Simple fetch + validate Resolve annotations
Discovery Multiple URLs, custom convention One URL serves all
Readability Rules scattered across files Self-documenting

Recommendation

  1. Publish source schemas with ucp_request/ucp_response annotations intact
  2. Remove the preprocessor and /spec/schemas/ directory
  3. Adopt portable validator for linting, resolution, validation.

Prototype validator: ucp-schema

# Lint schemas in CI
ucp-schema lint schemas/

# Resolve to vanilla JSON Schema (for tools that need it)
ucp-schema resolve checkout.json --request --op create

# Validate directly (self-describing from ucp.capabilities)
ucp-schema validate response.json --op read

The validator is written in Rust for portability & re-use. We can use it for our build process, and we can provide it as official binary for validating schemas for capability developers, and for agent+server developers to validate payloads on the wire — see docs on above repo for linting, resolving, validation, etc.

This PR

Big diff, but mostly deletions and simplification of the build pipeline in favor of using the shared ucp-schema validator.

  1. Integrates ucp-schema, deprecates Python preprocessor
  2. Publishes source schemas with annotations intact (non-breaking: standard validators ignore unknown keywords)
  3. Updates CI to use ucp-schema lint + ucp-schema validate

Big picture changes

1. Runtime schema resolution via ucp-schema CLI (main.py)

  • Calls ucp-schema resolve during mkdocs build
  • Caches resolved schemas per-schema to avoid redundant work
  • Detects polymorphic types for correct anchor generation

2. Versioned schema publishing (hooks.py)

  • Publishes source schemas to /schemas/{version}/ during build
  • Rewrites $id and $ref URLs to include version path
  • Sets version field in capability metadata
  • Handles date versions (2026-01-26) vs. named versions (draft)

3. CI integration (.github/workflows/docs.yml)

  • Installs ucp-schema via cargo install
  • Runs ucp-schema lint source/ before build
  • Schema errors fail the build

Before/After

Before:                              After:
source/schemas/checkout.json         source/schemas/checkout.json
       ↓                                    ↓
generate_schemas.py                  (direct publish)
       ↓                                    ↓
spec/schemas/checkout.json           https://ucp.dev/{version}/schemas/shopping/checkout.json
spec/schemas/checkout.create_req.json       (with annotations intact)
spec/schemas/checkout.update_req.json
spec/schemas/checkout_resp.json

Single schema URL, versioned. All site-generation markdown logic remains functional & intact.


Type of change

Please delete options that are not relevant.

  • Bug fix
  • New feature (non-breaking change which adds functionality)

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

Remove the pre-generated spec/ directory (95 JSON files, ~7,200 lines) and
resolve schemas at documentation build time using the ucp-schema CLI.

Changes:
- Delete spec/ directory and generation scripts (generate_schemas.py,
  schema_utils.py, validate_specs.py)
- Update main.py to call `ucp-schema resolve` with caching
- Add polymorphic type detection for correct anchor generation
- Update CI workflow to run `ucp-schema lint source/`
- Update CONTRIBUTING.md with new development workflow

The source/ directory is now the single source of truth. Schema resolution
happens on-demand during mkdocs build, with results cached per-schema.

Trade-offs:
- (+) Single source of truth in source/ directory
- (+) No drift between source and generated schemas
- (+) 7,200 fewer lines to maintain
- (+) Simpler workflow (no generate step before build)
- (-) Runtime dependency on ucp-schema CLI
Rewrites $id and $ref URLs to include version path so schemas resolve
correctly after mike deploys to /{version}/ paths.

Version handling:
- Date config (YYYY-MM-DD): used for both URL path and version field
- Non-date config (e.g., 'draft'): URL uses literal, version field
  gets today's date (publish date while marking as draft)

Changes:
- Add _rewrite_version_urls() to inject version into $id/$ref URLs
- Add _set_schema_version() to update capability version fields
- Remove redundant _publish_versioned_schemas() (was duplicating output)
- Set ucp_version to 'draft' for main branch builds

Example output for draft build:
  $id: https://ucp.dev/draft/schemas/shopping/checkout.json
  version: 2026-01-26
  $ref: https://ucp.dev/draft/schemas/shopping/types/buyer.json
hooks.py:
- Restructure version logic to make invariant explicit
  (url_version = ucp_version always when config exists)
- Replace string slicing with removeprefix() for clarity

main.py:
- Remove duplicate _resolve_with_ucp_schema() inner function
- Delegate to module-level _resolve_schema() instead
  Remove silent fallbacks that would render raw JSON with ucp_request
  annotations when ucp-schema CLI is unavailable or fails. Now displays
  visible error messages in docs with install instructions.

  Previously, if ucp-schema wasn't installed, the build would "succeed"
  but produce incorrect documentation with unprocessed annotations.
@igrigorik igrigorik added this to the Working Draft milestone Jan 27, 2026
@igrigorik igrigorik self-assigned this Jan 27, 2026
@igrigorik igrigorik requested a review from a team January 27, 2026 15:05
@igrigorik igrigorik added the TC review Ready for TC review label Jan 27, 2026
Copy link
Collaborator

@drewolson-google drewolson-google left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effectively solves the schema versioning problem, great work.

Two things to discuss at TC:

  1. This requires yet another language toolchain to be locally installed when working on UCP. We would now require python, node, and rust, along with other tools for running linting and other checks mirroring Github actions. Let's make sure the benefit of introducing another toolchain outweighs the costs.

  2. This makes ucp-schema a direct dependency of working on UCP. This means that we'll need to effectively be able to contribute to this tool if there are issues or bugs. While it removes a large amount of code from this repository, it does spread some of that logic of two repositories for the future contributors to this project.

if: steps.spec_files_changed.outputs.any_changed == 'true'
if: steps.source_files_changed.outputs.any_changed == 'true'
run: |
chmod +x scripts/ci_check_models.sh
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check the executable bit into git so we don't have to run this every time.

@igrigorik
Copy link
Contributor Author

@drewolson-google ack and +1 on toolchain.

I reached for Rust to make validator a self-contained binary that you can wget and not worry about installing an entire ecosystem — e.g. build artifacts that can be used directly. It makes authoring the validator harder, but that's pain and complexity that's localized to the ucp-schema devs, that everyone else does not and won't need to feel.

If we go forward with this path, I'd also suggest we adopt ucp-schema as official tool under UCP org -- happy to contribute and move it to the official repo.

hooks.py Outdated

def _set_schema_version(data, version):
"""Set version field in capability schemas (top-level 'version' property)."""
if "version" in data:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to remove version from the source file completely, and add it here for all files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 updated

Copy link
Collaborator

@wry-ry wry-ry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

  Remove version from source schema files. Build now injects version
  based on branch config (date-based) or today's date (non-date branches).

  Condition changed from "has version" to "has name" - named entities
  (capabilities, services, handlers) require version per entity meta-schema.
Integrate uv dependency management (PR #126) with ucp-schema toolchain:
- CONTRIBUTING.md: merged uv + ucp-schema development workflow
- Deleted generate_schemas.py, validate_specs.py (replaced by ucp-schema)
- Deleted requirements-docs.txt (replaced by uv sync)

Additional fixes:
- playground.md: versioned URLs pointing to 2026-01-23
- spec docs: removed hardcoded Version lines (now in URL/header only)
  The ci_check_models.sh script was designed for the old split-schema
  workflow (generate_schemas.py → spec/). It validated that
  datamodel-code-generator could parse the pre-split schemas.

  We need to rethink this in light of new shape. Removing, we can
  bring it back in separate PR.
  - Disable VALIDATE_SPELL_CODESPELL in super-linter (cspell already
    runs via spellcheck.yaml with better acronym handling)
  - Fix typo: wrappper → wrapper in .gitignore
@igrigorik igrigorik merged commit a8b185d into main Jan 28, 2026
16 checks passed
@igrigorik igrigorik deleted the feat/ucp-schema branch January 28, 2026 19:49
misza-one added a commit to misza-one/ucp that referenced this pull request Feb 8, 2026
The generate_ts_schema_types.js script still referenced the removed
spec/ directory. Updated to use source/ which is where schemas now
live after the migration in Universal-Commerce-Protocol#125.

Fixes Universal-Commerce-Protocol#150

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
misza-one added a commit to misza-one/ucp that referenced this pull request Feb 8, 2026
The generate_ts_schema_types.js script still referenced the removed
spec/ directory. Updated to use source/ which is where schemas now
live after the migration in Universal-Commerce-Protocol#125.

Fixes Universal-Commerce-Protocol#150
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants