Skip to main content
Glama
jettyio

croissant-validation

Official
by jettyio
README.md
# πŸ₯ croissant-validation

A **stateless MCP server** for validating [MLCommons Croissant](https://mlcommons.org/working-groups/data/croissant/) dataset metadata β€” built as a working demonstration of the [MCP 2026-07-28 specification](https://modelcontextprotocol.io/specification/2026-07-28), the revision that made the Model Context Protocol stateless.

**Live endpoint:** `https://croissant-validation.jetty.bot/mcp`

## Why this exists

The 2026-07-28 spec removed the `initialize`/`notifications/initialized` handshake and the `Mcp-Session-Id` header. Every request is now self-contained: protocol version and client capabilities travel in `_meta`, and servers advertise themselves via `server/discover`. That means an MCP server can run on plain serverless functions behind any load balancer β€” no sticky sessions, no shared session store.

This repo is exactly that: the [MCP Python SDK v2](https://github.com/modelcontextprotocol/python-sdk) (`mcp==2.0.0`, released alongside the spec) serving Croissant validation from Vercel serverless functions. Validation is performed by the official [`mlcroissant`](https://github.com/mlcommons/croissant/tree/main/python/mlcroissant) library β€” the same checks as the MLCommons croissant-validator, previously hosted in the [mlcbakery](https://github.com/jettyio/mlcbakery) MCP server.

## Tools

| Tool | Description |
|------|-------------|
| `validate_croissant` | Validate a Croissant JSON-LD document (object or JSON string) against the Croissant schema. Returns per-check results, blocking `errors`, and non-blocking `warnings`. |
| `validate_croissant_url` | Fetch metadata from a URL (e.g. a Hugging Face dataset's `/croissant` endpoint) and validate it. |
| `pdf_to_croissant` | Generate Croissant metadata from an academic paper: give it a PDF URL (or an `upload_id` from `POST /upload`, below) and a [Jetty](https://jetty.io) agent in an isolated sandbox reads the paper, extracts dataset metadata, writes `croissant.json`, and validates it β€” the MCP version of [mlcroissant.jetty.bot](https://mlcroissant.jetty.bot). Runs take 2–5 minutes. |
| `croissant_run_status` | Poll a running `pdf_to_croissant` job. Done when status is `completed` and `croissant.json` is in `files`. |
| `croissant_run_result` | Fetch an output file from a completed run β€” `croissant.json` comes back parsed and re-validated by this server's own validator. |

The generation tools mirror the API flow of [jettyio/pdf2croissant](https://github.com/jettyio/pdf2croissant) (upload β†’ runbook launch β†’ trajectory poll β†’ file download) and vendor its [runbook](croissant_mcp/pdf2croissant_runbook.md) verbatim. They need a `JETTY_API_TOKEN_PDF2CROISSANT` environment variable; without it, the validation tools still work β€” validation is pure and stateless.

## Local PDFs (`POST /upload`)

MCP has no client→server file-transfer primitive — tool arguments are JSON generated by the model, so base64-ing a paper into a tool call is a non-starter. Local files instead come in over plain HTTP:

```bash
curl -sS -F "file=@paper.pdf" https://croissant-validation.jetty.bot/upload
# β†’ {"upload_id": "…", "filename": "paper.pdf", "size_bytes": 123, "next": "…"}
```

Then call `pdf_to_croissant` with that `upload_id` instead of `pdf_url`. A raw-body POST works too (`--data-binary @paper.pdf`, optional `?filename=`).

The `upload_id` is the Jetty storage path, HMAC-signed with a key derived from the server's Jetty token β€” self-contained and tamper-evident, so the stateless server needs no session store, and callers can't mint references to arbitrary storage paths. Note the hosted instance sits behind Vercel's ~4.5 MB request-body cap; larger papers (up to 15 MB) should go through `pdf_url`.

## Connect

Claude Code:

```bash
claude mcp add --transport http croissant-validator https://croissant-validation.jetty.bot/mcp
```

Or any MCP client that speaks Streamable HTTP β€” clients on the 2025-era protocol still work; the SDK answers the legacy handshake alongside `server/discover`.

## One raw stateless request

No handshake β€” a single POST does everything:

```bash
curl -sS https://croissant-validation.jetty.bot/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "validate_croissant_url",
      "arguments": {"url": "https://huggingface.co/api/datasets/mnist/croissant"},
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientCapabilities": {}
      }
    }
  }'
```

The `params._meta` envelope replaces the old `initialize` handshake β€” the protocol version and client capabilities ride along on every request instead of being negotiated up front. (The server rejects 2026-07-28 requests without it.)

## Development

```bash
uv sync
uv run pytest -q                                  # validation + stateless HTTP round-trip tests
uv run uvicorn croissant_mcp.server:app --reload  # local server on :8000
```

Layout:

- `croissant_mcp/validation.py` β€” mlcroissant-backed validation (JSON well-formedness β†’ Croissant schema; warnings surfaced from `mlcroissant`'s issue tracker)
- `croissant_mcp/server.py` β€” `MCPServer` definition, tools, landing page, and the stateless Streamable HTTP ASGI app (`stateless_http=True, json_response=True`)
- `main.py` β€” Vercel entrypoint (the Python backend builder serves the ASGI app on all routes)
- `examples/` β€” a valid Croissant file ([Titanic, from the MLCommons repo](https://github.com/mlcommons/croissant/tree/main/datasets/1.0/titanic)) and an invalid variant (`invalid-not-a-dataset.json`, missing its `@type`)

Record-set generation checks (actually materializing data) are intentionally out of scope here β€” they can download arbitrarily large files, which doesn't belong in a serverless request. Schema validation is the static contract check.

## Deploy

Deployed on Vercel (Python runtime, Fluid Compute). The one serverless-specific consideration: `streamable_http_app()` starts its session manager via ASGI lifespan, which [Vercel now runs](https://vercel.com/changelog/fastapi-lifespan-events-are-now-supported-on-vercel). In stateless mode there is no cross-request state, so cold starts and horizontal scaling are free.

## Roadmap

- Wire into [Jetty](https://jetty.io) workflows as the validation step for an MCP-native version of [PDF β†’ Croissant](https://mlcroissant.jetty.bot).

## License

MIT