Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/check_marketplace_pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
from marketplace_pins_registry import ( # noqa: E402
check_registry_surface,
check_registry_version,
check_server_json_schema,
)
from marketplace_pins_self import FROZEN_PINS, check_self_pin # noqa: E402
from marketplace_pins_semver import ( # noqa: E402
Expand All @@ -135,6 +136,7 @@
"check_root_manifests",
"check_registry_surface",
"check_registry_version",
"check_server_json_schema",
"check_self_pin",
"latest_local_tag",
"main",
Expand Down
62 changes: 57 additions & 5 deletions scripts/marketplace_pins_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,69 @@ def check_registry_version(
)


# source: https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json
# definitions.ServerDetail.properties.description.{minLength,maxLength} —
# fetched and read directly 2026-08-10, not copied from a report of it.
# Real incident: ai-architect-mcp-codebase attempted to publish v0.9.1 with a
# 144-char description and got a live 422 from the registry — the tag that
# carries the violation can never be published; only a NEW tag with a
# shortened description is a way out (the fix existed on `main` but not in
# the tagged tree, so it published nothing). This offline check exists to
# catch the same defect before a tag is ever cut, not after.
SERVER_JSON_DESCRIPTION_MIN_LENGTH = 1
SERVER_JSON_DESCRIPTION_MAX_LENGTH = 100


def check_server_json_schema(root: Path) -> list[str]:
"""Offline, schema-derived validity checks on server.json's own fields
— distinct from check_registry_version's cross-repo VERSION check.
Catches the class of defect that produces a 422 at publish time (schema
violation), which a version-only comparison cannot see: a server.json
can have the exactly-correct version and still be unpublishable.

Reads whatever server.json is on disk for the ref this gate is running
against (the PR branch, or `main` on push/cron) — the tree a FUTURE tag
will be cut from, which is the only tree this offline check can affect.
An already-tagged historical commit is immutable; this cannot and does
not claim to fix one, only to stop the next one from repeating it.
"""
server_json = root / "server.json"
if not server_json.is_file():
return []
description = json.loads(server_json.read_text()).get("description", "")
length = len(description)
if length > SERVER_JSON_DESCRIPTION_MAX_LENGTH:
return [
f"SERVER_JSON_DESCRIPTION_TOO_LONG: server.json description is "
f"{length} chars; the MCP registry schema caps it at "
f"{SERVER_JSON_DESCRIPTION_MAX_LENGTH} — a tag cut from this "
f"tree cannot be published (422) and there is no way to publish "
f"an already-tagged violation after the fact, only a new tag"
]
if length < SERVER_JSON_DESCRIPTION_MIN_LENGTH:
return [
f"SERVER_JSON_DESCRIPTION_TOO_SHORT: server.json description is "
f"empty; the MCP registry schema requires at least "
f"{SERVER_JSON_DESCRIPTION_MIN_LENGTH} char"
]
return []


def check_registry_surface(root: Path, primary_pin: str):
"""Cross-check server.json's own registry `name` against what the
public MCP registry actually serves. Returns (failures, notices).
Absent server.json or a missing `name` field is not a failure — not
every repo on this marketplace publishes to the MCP registry.
public MCP registry actually serves, AND server.json's own schema
validity (description length). Returns (failures, notices). Absent
server.json or a missing `name` field skips the version check — not
every repo on this marketplace publishes to the MCP registry — but the
schema check still runs whenever server.json exists, name or not.
"""
server_json = root / "server.json"
if not server_json.is_file():
return [], []
schema_failures = check_server_json_schema(root)
registry_name = json.loads(server_json.read_text()).get("name", "")
if not registry_name:
return [], []
return schema_failures, []
failure, notice = check_registry_version(registry_name, primary_pin)
return ([failure] if failure else []), ([notice] if notice else [])
version_failures = [failure] if failure else []
return schema_failures + version_failures, ([notice] if notice else [])
88 changes: 79 additions & 9 deletions tests_py/scripts/test_check_marketplace_pins_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

from __future__ import annotations

import json
import unittest
import urllib.error
from pathlib import Path
from tempfile import TemporaryDirectory

from tests_py.scripts._marketplace_pins_test_loader import gate

Expand Down Expand Up @@ -102,22 +105,89 @@ def down(_search):

class TestRegistrySurface(unittest.TestCase):
def test_absent_server_json_is_not_a_failure(self):
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as d:
self.assertEqual(gate.check_registry_surface(Path(d), "1.0.0"), ([], []))

def test_missing_name_field_is_not_a_failure(self):
import json
from pathlib import Path
from tempfile import TemporaryDirectory

def test_missing_name_field_skips_version_check_not_schema_check(self):
with TemporaryDirectory() as d:
root = Path(d)
(root / "server.json").write_text(json.dumps({"version": "1.0.0"}))
(root / "server.json").write_text(
json.dumps({"version": "1.0.0", "description": "A valid description."})
)
self.assertEqual(gate.check_registry_surface(root, "1.0.0"), ([], []))


class TestServerJsonSchema(unittest.TestCase):
"""SERVER_JSON_DESCRIPTION_TOO_LONG/TOO_SHORT — offline, schema-derived
(maxLength=100, minLength=1, both read live from
https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json
2026-08-10). Distinct defect class from REGISTRY_VERSION_STALE: a
server.json can have the exactly-correct version and still be
unpublishable (422) because of an unrelated schema field.
"""

def test_absent_server_json_is_not_a_failure(self):
with TemporaryDirectory() as d:
self.assertEqual(gate.check_server_json_schema(Path(d)), [])

def test_compliant_description_passes(self):
with TemporaryDirectory() as d:
root = Path(d)
(root / "server.json").write_text(json.dumps({"description": "x" * 100}))
self.assertEqual(gate.check_server_json_schema(root), [])

def test_incident_replay_144_char_description_never_publishable(self):
"""The neighboring incident this check exists to close: a 422 at
publish time (ai-architect-mcp-codebase v0.9.1, 144-char
description) that could never be recovered from once tagged — the
fix landed on `main` only, not in the tagged tree. This test
proves the check fires BEFORE a tag would ever be cut.
"""
with TemporaryDirectory() as d:
root = Path(d)
description = "x" * 144
(root / "server.json").write_text(json.dumps({"description": description}))
failures = gate.check_server_json_schema(root)
self.assertEqual(len(failures), 1)
self.assertIn("SERVER_JSON_DESCRIPTION_TOO_LONG", failures[0])
self.assertIn("144", failures[0])
self.assertIn("100", failures[0])

def test_off_by_one_boundary(self):
with TemporaryDirectory() as d:
root = Path(d)
(root / "server.json").write_text(json.dumps({"description": "x" * 101}))
failures = gate.check_server_json_schema(root)
self.assertEqual(len(failures), 1)
self.assertIn("TOO_LONG", failures[0])

def test_empty_description_fails(self):
with TemporaryDirectory() as d:
root = Path(d)
(root / "server.json").write_text(json.dumps({"description": ""}))
failures = gate.check_server_json_schema(root)
self.assertEqual(len(failures), 1)
self.assertIn("SERVER_JSON_DESCRIPTION_TOO_SHORT", failures[0])

def test_missing_description_key_fails(self):
with TemporaryDirectory() as d:
root = Path(d)
(root / "server.json").write_text(json.dumps({"name": "x"}))
failures = gate.check_server_json_schema(root)
self.assertEqual(len(failures), 1)
self.assertIn("SERVER_JSON_DESCRIPTION_TOO_SHORT", failures[0])

def test_real_current_server_json_is_compliant(self):
"""Not a fixture — the actual committed server.json this PR ships.
Direct verification requested by review: fetched-at-tag v4.17.2 was
ALREADY compliant (95 chars, pre-dating this PR); this asserts the
working-tree copy this PR carries (98 chars) also is, which is what
the NEXT tag will publish.
"""
repo_root = Path(__file__).resolve().parents[2]
failures = gate.check_server_json_schema(repo_root)
self.assertEqual(failures, [], "server.json must stay registry-publishable")


if __name__ == "__main__":
unittest.main()