diff --git a/README.md b/README.md index e40a4ae1f..eb999fc8f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Docs

+ +

Docs · Install · @@ -238,6 +240,7 @@ shows an **Output Tokens Saved** card next to input compression, labelled Any OpenAI-compatible client works via `headroom proxy`. MCP-native: `headroom mcp install`. Undo durable wrapping with `headroom unwrap ` (supports: `claude`, `copilot`, `codex`, `opencode`, `openclaw`). +Registry authors can use the canonical [`server.json`](server.json) in the repo root instead of reconstructing the `headroom mcp serve` contract from prose. ### GitHub Copilot CLI subscription mode diff --git a/docs/content/docs/mcp.mdx b/docs/content/docs/mcp.mdx index 82bc5e597..bd6978fd2 100644 --- a/docs/content/docs/mcp.mdx +++ b/docs/content/docs/mcp.mdx @@ -121,6 +121,8 @@ headroom mcp serve --debug For MCP hosts that let you configure a local stdio server, point them at `headroom mcp serve`. If you also run the proxy, pass the proxy URL explicitly so retrieval and stats come from the intended proxy instance. +If you are publishing or consuming Headroom through an MCP registry, use the canonical descriptor at `https://github.com/headroomlabs-ai/headroom/blob/main/server.json`. It captures the package form for `headroom-ai[mcp]` and the current `headroom mcp serve` launch contract in one file. + ```json { "mcpServers": { diff --git a/headroom/mcp_registry/__init__.py b/headroom/mcp_registry/__init__.py index 7fbcf7f3d..e812359f2 100644 --- a/headroom/mcp_registry/__init__.py +++ b/headroom/mcp_registry/__init__.py @@ -26,6 +26,7 @@ from .install import ( install_everywhere, ) from .opencode import OpencodeRegistrar +from .server_json import build_server_json, render_server_json __all__ = [ "DEFAULT_PROXY_URL", @@ -39,9 +40,11 @@ __all__ = [ "any_succeeded", "build_headroom_spec", "build_serena_spec", + "build_server_json", "build_tokensave_spec", "format_result", "format_results", "get_all_registrars", "install_everywhere", + "render_server_json", ] diff --git a/headroom/mcp_registry/server_json.py b/headroom/mcp_registry/server_json.py new file mode 100644 index 000000000..0fa90b598 --- /dev/null +++ b/headroom/mcp_registry/server_json.py @@ -0,0 +1,128 @@ +"""Canonical MCP publication metadata for the Headroom server.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover + import tomli as tomllib # type: ignore[no-redef] + +from .install import build_headroom_spec + +SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json" +SERVER_NAME = "io.github.headroomlabs-ai/headroom" +SERVER_TITLE = "Headroom" +SERVER_DESCRIPTION = ( + "Headroom MCP server for compression, retrieval, and stats in MCP-compatible hosts." +) +PYPI_OWNERSHIP_MARKER = f"" +WEBSITE_URL = "https://headroomlabs-ai.github.io/headroom/" +REPOSITORY_URL = "https://github.com/headroomlabs-ai/headroom" +REPOSITORY_ID = "1129940957" +PYPI_REGISTRY_URL = "https://pypi.org" + + +@dataclass(frozen=True) +class ProjectMetadata: + """Package metadata needed for the published MCP descriptor.""" + + package_name: str + version: str + + +def _project_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def load_project_metadata(pyproject_path: Path | None = None) -> ProjectMetadata: + """Load the publishable package metadata from ``pyproject.toml``.""" + + path = pyproject_path or (_project_root() / "pyproject.toml") + data = tomllib.loads(path.read_text(encoding="utf-8")) + project = data["project"] + return ProjectMetadata( + package_name=project["name"], + version=project["version"], + ) + + +def _build_runtime_contract() -> tuple[str, tuple[str, str]]: + """Return the canonical CLI entrypoint plus the MCP subcommand tail.""" + + spec = build_headroom_spec() + if spec.name != "headroom": + raise ValueError(f"unexpected MCP server name: {spec.name}") + runtime_tail = spec.args[-2:] + if runtime_tail != ("mcp", "serve"): + raise ValueError(f"unexpected MCP launch tail: {spec.args}") + return spec.name, (runtime_tail[0], runtime_tail[1]) + + +def _build_mcp_package_spec(metadata: ProjectMetadata) -> str: + """Return the PyPI requirement needed to launch ``headroom mcp serve``.""" + + return f"{metadata.package_name}[mcp]" + + +def build_server_json(metadata: ProjectMetadata | None = None) -> dict[str, object]: + """Build the canonical ``server.json`` payload for Headroom.""" + + metadata = metadata or load_project_metadata() + command_name, runtime_tail = _build_runtime_contract() + package_spec = _build_mcp_package_spec(metadata) + return { + "$schema": SCHEMA_URL, + "name": SERVER_NAME, + "description": SERVER_DESCRIPTION, + "title": SERVER_TITLE, + "websiteUrl": WEBSITE_URL, + "repository": { + "url": REPOSITORY_URL, + "source": "github", + "id": REPOSITORY_ID, + }, + "version": metadata.version, + "packages": [ + { + "registryType": "pypi", + "registryBaseUrl": PYPI_REGISTRY_URL, + "identifier": metadata.package_name, + "version": metadata.version, + "runtimeHint": "uvx", + # Current uvx needs --from when the package name and script name differ. + "runtimeArguments": [ + { + "type": "named", + "name": "--from", + "value": package_spec, + } + ], + "transport": { + "type": "stdio", + }, + "packageArguments": [ + { + "type": "positional", + "value": command_name, + }, + *( + { + "type": "positional", + "value": value, + } + for value in runtime_tail + ), + ], + } + ], + } + + +def render_server_json(metadata: ProjectMetadata | None = None) -> str: + """Render the canonical ``server.json`` payload with stable formatting.""" + + return json.dumps(build_server_json(metadata), indent=2) + "\n" diff --git a/server.json b/server.json new file mode 100644 index 000000000..c660bf2fe --- /dev/null +++ b/server.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.headroomlabs-ai/headroom", + "description": "Headroom MCP server for compression, retrieval, and stats in MCP-compatible hosts.", + "title": "Headroom", + "websiteUrl": "https://headroomlabs-ai.github.io/headroom/", + "repository": { + "url": "https://github.com/headroomlabs-ai/headroom", + "source": "github", + "id": "1129940957" + }, + "version": "0.27.0", + "packages": [ + { + "registryType": "pypi", + "registryBaseUrl": "https://pypi.org", + "identifier": "headroom-ai", + "version": "0.27.0", + "runtimeHint": "uvx", + "runtimeArguments": [ + { + "type": "named", + "name": "--from", + "value": "headroom-ai[mcp]" + } + ], + "transport": { + "type": "stdio" + }, + "packageArguments": [ + { + "type": "positional", + "value": "headroom" + }, + { + "type": "positional", + "value": "mcp" + }, + { + "type": "positional", + "value": "serve" + } + ] + } + ] +} diff --git a/tests/test_mcp_registry/test_server_json.py b/tests/test_mcp_registry/test_server_json.py new file mode 100644 index 000000000..04f255eb6 --- /dev/null +++ b/tests/test_mcp_registry/test_server_json.py @@ -0,0 +1,86 @@ +"""Tests for the canonical MCP server.json descriptor.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from headroom.mcp_registry import build_server_json, render_server_json +from headroom.mcp_registry.install import build_headroom_spec +from headroom.mcp_registry.server_json import ( + PYPI_OWNERSHIP_MARKER, + REPOSITORY_ID, + REPOSITORY_URL, + SCHEMA_URL, + SERVER_DESCRIPTION, + SERVER_NAME, + WEBSITE_URL, + _build_mcp_package_spec, + load_project_metadata, +) + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +def test_build_server_json_uses_project_metadata() -> None: + metadata = load_project_metadata() + descriptor = build_server_json(metadata) + + assert descriptor["$schema"] == SCHEMA_URL + assert descriptor["name"] == SERVER_NAME + assert descriptor["description"] == SERVER_DESCRIPTION + assert descriptor["version"] == metadata.version + assert descriptor["websiteUrl"] == WEBSITE_URL + assert descriptor["repository"] == { + "url": REPOSITORY_URL, + "source": "github", + "id": REPOSITORY_ID, + } + + package = descriptor["packages"][0] + assert package["registryType"] == "pypi" + assert package["registryBaseUrl"] == "https://pypi.org" + assert package["identifier"] == metadata.package_name + assert package["version"] == metadata.version + assert package["runtimeArguments"] == [ + { + "type": "named", + "name": "--from", + "value": _build_mcp_package_spec(metadata), + } + ] + + +def test_build_server_json_matches_runtime_contract() -> None: + descriptor = build_server_json() + runtime = build_headroom_spec() + package = descriptor["packages"][0] + + assert package["runtimeHint"] == "uvx" + assert package["runtimeArguments"] == [ + { + "type": "named", + "name": "--from", + "value": _build_mcp_package_spec(load_project_metadata()), + } + ] + assert [arg["value"] for arg in package["packageArguments"]] == [ + runtime.name, + *runtime.args[-2:], + ] + assert package["transport"] == {"type": "stdio"} + + +def test_root_server_json_matches_builder() -> None: + artifact = PROJECT_ROOT / "server.json" + assert artifact.read_text(encoding="utf-8") == render_server_json() + assert json.loads(artifact.read_text(encoding="utf-8")) == build_server_json() + + +def test_docs_point_to_canonical_server_json() -> None: + readme = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8") + mcp_docs = (PROJECT_ROOT / "docs/content/docs/mcp.mdx").read_text(encoding="utf-8") + + assert PYPI_OWNERSHIP_MARKER in readme + assert "`server.json`" in readme + assert "https://github.com/headroomlabs-ai/headroom/blob/main/server.json" in mcp_docs